Command Triggers

Started by Dimitri Fontaineabout 14 years ago135 messages
#1Dimitri Fontaine
dimitri@2ndQuadrant.fr
1 attachment(s)

Hi,

As proposed by Jan Wieck on hackers and discussed in Ottawa at the
Clusters Hackers Meeting, the most (only?) workable way to ever have DDL
triggers is to have "command triggers" instead.

Rather than talking about catalogs and the like, it's all about firing a
registered user-defined function before or after processing the utility
command, or instead of processing it. This naturally involves a new
catalog (pg_cmdtrigger) and some new subcommands of CREATE and DROP
TRIGGER, and some support code for calling the function at the right
time.

The right place to hook this code in seems to be ProcessUtility(), which
is a central place for about all the commands that we handle. An
exception to that rule would be SELECT INTO and CREATE TABLE AS SELECT,
and my proposal would be to add specific call sites to the functions
I've provided in the attached patch rather than try to contort them into
being a good citizen as a utility command.

The ProcessUtility() integration currently looks like that:

const char *commandTag = CreateCommandTag(parsetree);
if (ExecBeforeOrInsteadOfCommandTriggers(parsetree, commandTag) > 0)
return;

... current code ...

ExecAfterCommandTriggers(parsetree, commandTag);

So I guess adding some call sites is manageable.

Now, the patch contains a Proof Of Concept implementation with a small
level of documentation and regression tests in order to get an agreement
on the principles that we already discussed.

The other part of the command trigger facility is what information we
should give to the user-defined functions, and in which form. I've
settled on passing always 4 text arguments: the command string, the
parse tree as a nodeToString() representation (Jan believes that this is
the easiest form we can provide for code consumption, and I tend to
agree), the schema name or NULL if the targeted object of the command is
not qualified, and the object name (or NULL if that does not apply).

CREATE FUNCTION cmdtrigger_notice
(
IN cmd_string text,
IN cmd_nodestring text,
IN schemaname text,
IN relname text
)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
RAISE NOTICE 'cmd_string: %', cmd_string;
END;
$$;

CREATE TRIGGER cmdtrigger_notice
AFTER COMMAND CREATE TABLE
EXECUTE PROCEDURE cmdtrigger_notice();

The v1 patch attached contains implementation for some commands only.
We need to add rewriting facilities for those commands we want to
support in the proposed model, because of multi-queries support in the
protocol and dynamic queries in EXECUTE e.g. (though I admit not having
had a look at EXECUTE implementation).

So each supported command has a cost, and the ones I claim to support in
the grammar in the patch are not seeing a complete support: first, I'm
missing some outfuncs and readfuncs (but I believe we can complete that
using a script on the source code) so that the cmd_nodestring argument
is currently always NULL. Second, I didn't complete the rewriting of
some more complex commands such as CREATE TABLE and ALTER TABLE.

Note that we can't reuse that much of ruleutils.c because it's written
to work from what we store in the catalogs rather than from a parsetree
object.

So, any comment on the approach before I complete the rewriting of the
commands, the out/read funcs, and add more commands to the trigger
support code?

https://github.com/dimitri/postgres/compare/master...command_triggers

$ git diff --stat master..
doc/src/sgml/ref/create_trigger.sgml | 97 +++++-
doc/src/sgml/ref/drop_trigger.sgml | 19 +-
src/backend/catalog/Makefile | 2 +-
src/backend/catalog/dependency.c | 47 ++-
src/backend/catalog/objectaddress.c | 8 +
src/backend/commands/Makefile | 4 +-
src/backend/commands/cmdtrigger.c | 580 ++++++++++++++++++++++++++
src/backend/nodes/copyfuncs.c | 32 ++
src/backend/nodes/equalfuncs.c | 28 ++
src/backend/nodes/outfuncs.c | 405 ++++++++++++++++++
src/backend/parser/gram.y | 70 +++-
src/backend/tcop/utility.c | 44 ++
src/backend/utils/adt/ruleutils.c | 613 +++++++++++++++++++++++++++-
src/include/catalog/dependency.h | 1 +
src/include/catalog/indexing.h | 5 +
src/include/catalog/pg_cmdtrigger.h | 59 +++
src/include/commands/cmdtrigger.h | 43 ++
src/include/commands/defrem.h | 14 +
src/include/nodes/nodes.h | 2 +
src/include/nodes/parsenodes.h | 28 ++
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/sanity_check.out | 3 +-
src/test/regress/expected/triggers.out | 35 ++
src/test/regress/sql/triggers.sql | 35 ++
24 files changed, 2157 insertions(+), 18 deletions(-)

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

Attachments:

command-trigger.v1.patchtext/x-patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index a7915cf..059e6bb 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -29,12 +29,26 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
     [ WHEN ( <replaceable class="parameter">condition</replaceable> ) ]
     EXECUTE PROCEDURE <replaceable class="PARAMETER">function_name</replaceable> ( <replaceable class="PARAMETER">arguments</replaceable> )
 
+CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTER | INSTEAD OF } COMMAND <replaceable class="PARAMETER">command</replaceable>
+    EXECUTE PROCEDURE <replaceable class="PARAMETER">function_name</replaceable> ( <replaceable class="PARAMETER">arguments</replaceable> )
+
 <phrase>where <replaceable class="parameter">event</replaceable> can be one of:</phrase>
 
     INSERT
     UPDATE [ OF <replaceable class="parameter">column_name</replaceable> [, ... ] ]
     DELETE
     TRUNCATE
+
+<phrase>and where <replaceable class="parameter">command</replaceable> can be one of:</phrase>
+
+    CREATE TABLE
+    ALTER TABLE
+    DROP TABLE
+    CREATE VIEW
+    DROP VIEW
+    CREATE EXTENSION
+    DROP EXTENSION
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,10 +56,20 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
   <title>Description</title>
 
   <para>
-   <command>CREATE TRIGGER</command> creates a new trigger.  The
-   trigger will be associated with the specified table or view and will
-   execute the specified function <replaceable
-   class="parameter">function_name</replaceable> when certain events occur.
+   <command>CREATE TRIGGER</command> creates a new trigger. The trigger will
+   be associated with the specified table, view or command and will execute
+   the specified
+   function <replaceable class="parameter">function_name</replaceable> when
+   certain events occur.
+  </para>
+
+  <para>
+   The command trigger can be specified to fire before or after the command
+   is executed, or instead of executing the command. A before trigger's
+   function must return a boolean, returning <literal>False</literal> allows
+   the procedure to prevent the command execution. AFTER and INSTEAD OF
+   triggers return value is not used, such trigger's function must then
+   return <literal>void</literal>.
   </para>
 
   <para>
@@ -251,6 +275,15 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
    </varlistentry>
 
    <varlistentry>
+    <term><replaceable class="parameter">command</replaceable></term>
+    <listitem>
+     <para>
+      The tag of the command the trigger is for.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><replaceable class="PARAMETER">referenced_table_name</replaceable></term>
     <listitem>
      <para>
@@ -334,7 +367,23 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
      <para>
       A user-supplied function that is declared as taking no arguments
       and returning type <literal>trigger</>, which is executed when
-      the trigger fires.
+      the trigger fires, for table and view triggers.
+     </para>
+     <para>
+      In the case of a BEFORE COMMAND trigger, the user-supplied function
+      must be declared as taking 4 text arguments and returning a boolean.
+      In the case of an AFTER COMMAND trigger or an INSTEAD OF trigger, the
+      user-supplied function must be declare as taking 4 text arguments and
+      returning void.
+     </para>
+     <para>
+      The command trigger function is called with the
+      parameters <literal>command string</literal> (normalized command
+      string rewritten by PostgreSQL), <literal>command node
+      string</literal> (the command syntax tree, in its text form, is easier
+      to deal with programmaticaly for more complex inquiries about the
+      command), <literal>schemaname</literal> (can be null)
+      and <literal>object name</literal>.
      </para>
     </listitem>
    </varlistentry>
@@ -352,6 +401,10 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
       can be accessed within the function; it might be different from
       normal function arguments.
      </para>
+     <para>
+      Command triggers pay no attention to
+      the <replaceable class="parameter">arguments</replaceable>.
+     </para>
     </listitem>
    </varlistentry>
   </variablelist>
@@ -469,6 +522,35 @@ CREATE TRIGGER view_insert
     FOR EACH ROW
     EXECUTE PROCEDURE view_insert_row();
 </programlisting>
+
+   Execute the function <function>enforce_local_style</> each time
+   a <literal>CREATE TABLE</literal> command is run:
+
+<programlisting>
+CREATE OR REPLACE FUNCTION enforce_local_style
+ (
+   IN cmd_string     text,
+   IN cmd_nodestring text,
+   IN schemaname     text,
+   IN relname        text
+ )
+ RETURNS bool
+ LANGUAGE plpgsql
+AS $$
+BEGIN
+  IF substring(relname, 0, 4) NOT IN ('ab_', 'cz_', 'fr_')
+  THEN
+    RAISE WARNING 'invalid relation name: %', relname;
+    RETURN FALSE;
+  END IF;
+  RETURN TRUE;
+END;
+$$;
+
+CREATE TRIGGER check_style
+        BEFORE COMMAND CREATE TABLE
+       EXECUTE PROCEDURE enforce_local_style();
+</programlisting>
   </para>
 
   <para>
@@ -531,6 +613,11 @@ CREATE TRIGGER view_insert
   </para>
 
   <para>
+   The ability to run triggers on commands is <productname>PostgreSQL</>
+   extension of the SQL standard.
+  </para>
+
+  <para>
    The ability to specify multiple actions for a single trigger using
    <literal>OR</literal> is a <productname>PostgreSQL</> extension of
    the SQL standard.
diff --git a/doc/src/sgml/ref/drop_trigger.sgml b/doc/src/sgml/ref/drop_trigger.sgml
index 2fcc690..3e37aa2 100644
--- a/doc/src/sgml/ref/drop_trigger.sgml
+++ b/doc/src/sgml/ref/drop_trigger.sgml
@@ -22,6 +22,18 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 DROP TRIGGER [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable> ON <replaceable class="PARAMETER">table</replaceable> [ CASCADE | RESTRICT ]
+DROP TRIGGER [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable> ON COMMAND <replaceable class="PARAMETER">command</replaceable> [ CASCADE | RESTRICT ]
+
+<phrase>where <replaceable class="parameter">command</replaceable> can be one of:</phrase>
+
+    CREATE TABLE
+    ALTER TABLE
+    DROP TABLE
+    CREATE VIEW
+    DROP VIEW
+    CREATE EXTENSION
+    DROP EXTENSION
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -29,9 +41,10 @@ DROP TRIGGER [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable> ON
   <title>Description</title>
 
   <para>
-   <command>DROP TRIGGER</command> removes an existing
-   trigger definition. To execute this command, the current
-   user must be the owner of the table for which the trigger is defined.
+   <command>DROP TRIGGER</command> removes an existing trigger definition.
+   To execute this command, the current user must be the owner of the table
+   for which the trigger is defined, or a database owner in case of a
+   command trigger.
   </para>
  </refsect1>
 
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 5a4419d..7e7d4c6 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -31,7 +31,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_attrdef.h pg_constraint.h pg_inherits.h pg_index.h pg_operator.h \
 	pg_opfamily.h pg_opclass.h pg_am.h pg_amop.h pg_amproc.h \
 	pg_language.h pg_largeobject_metadata.h pg_largeobject.h pg_aggregate.h \
-	pg_statistic.h pg_rewrite.h pg_trigger.h pg_description.h \
+	pg_statistic.h pg_rewrite.h pg_trigger.h pg_cmdtrigger.h pg_description.h \
 	pg_cast.h pg_enum.h pg_namespace.h pg_conversion.h pg_depend.h \
 	pg_database.h pg_db_role_setting.h pg_tablespace.h pg_pltemplate.h \
 	pg_authid.h pg_auth_members.h pg_shdepend.h pg_shdescription.h \
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 223c097..42701f3 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -25,6 +25,7 @@
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_cmdtrigger.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_collation_fn.h"
 #include "catalog/pg_constraint.h"
@@ -52,6 +53,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "commands/cmdtrigger.h"
 #include "commands/comment.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
@@ -157,7 +159,8 @@ static const Oid object_classes[MAX_OCLASS] = {
 	ForeignServerRelationId,	/* OCLASS_FOREIGN_SERVER */
 	UserMappingRelationId,		/* OCLASS_USER_MAPPING */
 	DefaultAclRelationId,		/* OCLASS_DEFACL */
-	ExtensionRelationId			/* OCLASS_EXTENSION */
+	ExtensionRelationId,		/* OCLASS_EXTENSION */
+	CmdTriggerRelationId		/* OCLASS_CMDTRIGGER */
 };
 
 
@@ -1052,6 +1055,10 @@ doDeletion(const ObjectAddress *object)
 				break;
 			}
 
+		case OCLASS_CMDTRIGGER:
+			RemoveCmdTriggerById(object->objectId);
+			break;
+
 		case OCLASS_PROC:
 			RemoveFunctionById(object->objectId);
 			break;
@@ -2173,6 +2180,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case ExtensionRelationId:
 			return OCLASS_EXTENSION;
+
+		case CmdTriggerRelationId:
+			return OCLASS_CMDTRIGGER;
 	}
 
 	/* shouldn't get here */
@@ -2807,6 +2817,41 @@ getObjectDescription(const ObjectAddress *object)
 				break;
 			}
 
+        case OCLASS_CMDTRIGGER:
+			{
+				Relation	trigDesc;
+				ScanKeyData skey[1];
+				SysScanDesc tgscan;
+				HeapTuple	tup;
+				Form_pg_cmdtrigger trig;
+
+				trigDesc = heap_open(CmdTriggerRelationId, AccessShareLock);
+
+				ScanKeyInit(&skey[0],
+							ObjectIdAttributeNumber,
+							BTEqualStrategyNumber, F_OIDEQ,
+							ObjectIdGetDatum(object->objectId));
+
+				tgscan = systable_beginscan(trigDesc, CmdTriggerOidIndexId, true,
+											SnapshotNow, 1, skey);
+
+				tup = systable_getnext(tgscan);
+
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "could not find tuple for command trigger %u",
+						 object->objectId);
+
+				trig = (Form_pg_cmdtrigger) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("trigger %s on %s"),
+								 NameStr(trig->ctgname),
+								 NameStr(trig->ctgcommand));
+
+				systable_endscan(tgscan);
+				heap_close(trigDesc, AccessShareLock);
+				break;
+			}
+
 		default:
 			appendStringInfo(&buffer, "unrecognized object %u %u %d",
 							 object->classId,
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index ec4c987..8c4c4f8 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -21,6 +21,7 @@
 #include "catalog/objectaddress.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_cmdtrigger.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
@@ -44,6 +45,7 @@
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
+#include "commands/cmdtrigger.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/extension.h"
@@ -203,6 +205,12 @@ static ObjectPropertyType ObjectProperty[] =
 		InvalidAttrNumber
 	},
 	{
+		CmdTriggerRelationId,
+		CmdTriggerOidIndexId,
+		-1,
+		InvalidAttrNumber
+	},
+	{
 		TSConfigRelationId,
 		TSConfigOidIndexId,
 		TSCONFIGOID,
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 4af7aad..a00a358 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -12,8 +12,8 @@ subdir = src/backend/commands
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
-OBJS = aggregatecmds.o alter.o analyze.o async.o cluster.o comment.o  \
-	collationcmds.o constraint.o conversioncmds.o copy.o \
+OBJS = aggregatecmds.o alter.o analyze.o async.o cluster.o cmdtrigger.o \
+	comment.o collationcmds.o constraint.o conversioncmds.o copy.o \
 	dbcommands.o define.o discard.o dropcmds.o explain.o extension.o \
 	foreigncmds.o functioncmds.o \
 	indexcmds.o lockcmds.o operatorcmds.o opclasscmds.o \
diff --git a/src/backend/commands/cmdtrigger.c b/src/backend/commands/cmdtrigger.c
new file mode 100644
index 0000000..b8fd250
--- /dev/null
+++ b/src/backend/commands/cmdtrigger.c
@@ -0,0 +1,580 @@
+/*-------------------------------------------------------------------------
+ *
+ * trigger.c
+ *	  PostgreSQL TRIGGERs support code.
+ *
+ * Portions Copyright (c) 1996-2011, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/trigger.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/sysattr.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_cmdtrigger.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_trigger.h"
+#include "catalog/pg_type.h"
+#include "commands/defrem.h"
+#include "commands/cmdtrigger.h"
+#include "commands/trigger.h"
+#include "parser/parse_func.h"
+#include "pgstat.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/tqual.h"
+
+static RegProcedure * list_triggers_for_command(const char *command, char type);
+
+/*
+ * Create a trigger.  Returns the OID of the created trigger.
+ */
+Oid
+CreateCmdTrigger(CreateCmdTrigStmt *stmt, const char *queryString)
+{
+	Relation	tgrel;
+	SysScanDesc tgscan;
+	ScanKeyData key;
+	HeapTuple	tuple;
+	Datum		values[Natts_pg_trigger];
+	bool		nulls[Natts_pg_trigger];
+	/* cmd trigger args: cmd_string, cmd_nodestring, schemaname, objectname */
+	Oid			fargtypes[4] = {TEXTOID, TEXTOID, TEXTOID, TEXTOID};
+	Oid			funcoid;
+	Oid			funcrettype;
+	Oid			trigoid;
+	char        ctgtype;
+	ObjectAddress myself,
+				referenced;
+
+	/*
+	 * Find and validate the trigger function.
+	 */
+	funcoid = LookupFuncName(stmt->funcname, 4, fargtypes, false);
+	funcrettype = get_func_rettype(funcoid);
+
+	/*
+	 * Generate the trigger's OID now, so that we can use it in the name if
+	 * needed.
+	 */
+	tgrel = heap_open(CmdTriggerRelationId, RowExclusiveLock);
+
+	/*
+	 * Scan pg_cmdtrigger for existing triggers on command. We do this only to
+	 * give a nice error message if there's already a trigger of the same name.
+	 * (The unique index on ctgcommand/ctgname would complain anyway.)
+	 *
+	 * NOTE that this is cool only because we have AccessExclusiveLock on
+	 * the relation, so the trigger set won't be changing underneath us.
+	 */
+	ScanKeyInit(&key,
+				Anum_pg_cmdtrigger_ctgcommand,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(stmt->command));
+	tgscan = systable_beginscan(tgrel, CmdTriggerCommandNameIndexId, true,
+								SnapshotNow, 1, &key);
+	while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
+	{
+		Form_pg_cmdtrigger pg_cmdtrigger = (Form_pg_cmdtrigger) GETSTRUCT(tuple);
+
+		if (namestrcmp(&(pg_cmdtrigger->ctgname), stmt->trigname) == 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("trigger \"%s\" for command \"%s\" already exists",
+							stmt->trigname, stmt->command)));
+	}
+	systable_endscan(tgscan);
+
+	switch (stmt->timing)
+	{
+		case TRIGGER_TYPE_BEFORE:
+		{
+	        RegProcedure *procs = list_triggers_for_command(stmt->command, CMD_TRIGGER_FIRED_INSTEAD);
+		    ctgtype = CMD_TRIGGER_FIRED_BEFORE;
+			if (procs[0] != InvalidOid)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("\"%s\" already has INSTEAD OF triggers",
+								stmt->command),
+						 errdetail("Commands cannot have both BEFORE and INSTEAD OF triggers.")));
+            break;
+		}
+
+		case TRIGGER_TYPE_INSTEAD:
+		{
+	        RegProcedure *before = list_triggers_for_command(stmt->command, CMD_TRIGGER_FIRED_BEFORE);
+	        RegProcedure *after;
+		    ctgtype = CMD_TRIGGER_FIRED_INSTEAD;
+			if (before[0] != InvalidOid)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("\"%s\" already has BEFORE triggers",
+								stmt->command),
+						 errdetail("Commands cannot have both BEFORE and INSTEAD OF triggers.")));
+
+			after = list_triggers_for_command(stmt->command, CMD_TRIGGER_FIRED_AFTER);
+			if (after[0] != InvalidOid)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("\"%s\" already has AFTER triggers",
+								stmt->command),
+						 errdetail("Commands cannot have both AFTER and INSTEAD OF triggers.")));
+            break;
+		}
+
+		case TRIGGER_TYPE_AFTER:
+		{
+	        RegProcedure *procs = list_triggers_for_command(stmt->command, CMD_TRIGGER_FIRED_INSTEAD);
+		    ctgtype = CMD_TRIGGER_FIRED_AFTER;
+			if (procs[0] != InvalidOid)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("\"%s\" already has INSTEAD OF triggers",
+								stmt->command),
+						 errdetail("Commands cannot have both AFTER and INSTEAD OF triggers.")));
+            break;
+		}
+	}
+
+	if (ctgtype == CMD_TRIGGER_FIRED_BEFORE && funcrettype != BOOLOID)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("function \"%s\" must return type \"boolean\"",
+						NameListToString(stmt->funcname))));
+
+	if (ctgtype != CMD_TRIGGER_FIRED_BEFORE && funcrettype != VOIDOID)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("function \"%s\" must return type \"void\"",
+						NameListToString(stmt->funcname))));
+
+	/*
+	 * Build the new pg_trigger tuple.
+	 */
+	memset(nulls, false, sizeof(nulls));
+
+	values[Anum_pg_cmdtrigger_ctgcommand - 1] = NameGetDatum(stmt->command);
+	values[Anum_pg_cmdtrigger_ctgname - 1] = NameGetDatum(stmt->trigname);
+	values[Anum_pg_cmdtrigger_ctgfoid - 1] = ObjectIdGetDatum(funcoid);
+	values[Anum_pg_cmdtrigger_ctgtype - 1] = CharGetDatum(ctgtype);
+	values[Anum_pg_cmdtrigger_ctgenabled - 1] = CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
+
+	tuple = heap_form_tuple(tgrel->rd_att, values, nulls);
+
+	/* force tuple to have the desired OID */
+	trigoid = HeapTupleGetOid(tuple);
+
+	/*
+	 * Insert tuple into pg_trigger.
+	 */
+	simple_heap_insert(tgrel, tuple);
+
+	CatalogUpdateIndexes(tgrel, tuple);
+
+	heap_freetuple(tuple);
+	heap_close(tgrel, RowExclusiveLock);
+
+	/*
+	 * Record dependencies for trigger.  Always place a normal dependency on
+	 * the function.
+	 */
+	myself.classId = TriggerRelationId;
+	myself.objectId = trigoid;
+	myself.objectSubId = 0;
+
+	referenced.classId = ProcedureRelationId;
+	referenced.objectId = funcoid;
+	referenced.objectSubId = 0;
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	return trigoid;
+}
+
+/*
+ * DropTrigger - drop an individual trigger by name
+ */
+void
+DropCmdTrigger(DropCmdTrigStmt *stmt)
+{
+	ObjectAddress object;
+
+	object.classId = CmdTriggerRelationId;
+	object.objectId = get_cmdtrigger_oid(stmt->trigname, stmt->command,
+										 stmt->missing_ok);
+	object.objectSubId = 0;
+
+	if (!OidIsValid(object.objectId))
+	{
+		ereport(NOTICE,
+		  (errmsg("trigger \"%s\" for command \"%s\" does not exist, skipping",
+				  stmt->trigname, stmt->command)));
+		return;
+	}
+
+	/*
+	 * Do the deletion
+	 */
+	performDeletion(&object, stmt->behavior);
+}
+
+/*
+ * Guts of command trigger deletion.
+ */
+void
+RemoveCmdTriggerById(Oid trigOid)
+{
+	Relation	tgrel;
+	SysScanDesc tgscan;
+	ScanKeyData skey[1];
+	HeapTuple	tup;
+
+	tgrel = heap_open(CmdTriggerRelationId, RowExclusiveLock);
+
+	/*
+	 * Find the trigger to delete.
+	 */
+	ScanKeyInit(&skey[0],
+				ObjectIdAttributeNumber,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(trigOid));
+
+	tgscan = systable_beginscan(tgrel, CmdTriggerOidIndexId, true,
+								SnapshotNow, 1, skey);
+
+	tup = systable_getnext(tgscan);
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "could not find tuple for command trigger %u", trigOid);
+
+	/*
+	 * Delete the pg_cmdtrigger tuple.
+	 */
+	simple_heap_delete(tgrel, &tup->t_self);
+
+	systable_endscan(tgscan);
+	heap_close(tgrel, RowExclusiveLock);
+}
+
+/*
+ * get_cmdtrigger_oid - Look up a trigger by name to find its OID.
+ *
+ * If missing_ok is false, throw an error if trigger not found.  If
+ * true, just return InvalidOid.
+ */
+Oid
+get_cmdtrigger_oid(const char *trigname, const char *command, bool missing_ok)
+{
+	Relation	tgrel;
+	ScanKeyData skey[2];
+	SysScanDesc tgscan;
+	HeapTuple	tup;
+	Oid			oid;
+
+	/*
+	 * Find the trigger, verify permissions, set up object address
+	 */
+	tgrel = heap_open(CmdTriggerRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_cmdtrigger_ctgcommand,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(command));
+	ScanKeyInit(&skey[1],
+				Anum_pg_cmdtrigger_ctgname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(trigname));
+
+	tgscan = systable_beginscan(tgrel, CmdTriggerCommandNameIndexId, true,
+								SnapshotNow, 1, skey);
+
+	tup = systable_getnext(tgscan);
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("trigger \"%s\" for command \"%s\" does not exist, skipping",
+							 trigname, command)));
+		oid = InvalidOid;
+	}
+	else
+	{
+		oid = HeapTupleGetOid(tup);
+	}
+
+	systable_endscan(tgscan);
+	heap_close(tgrel, AccessShareLock);
+	return oid;
+}
+
+
+/*
+ * Functions to execute the command triggers.
+ *
+ * We call the functions that matches the command triggers definitions in
+ * alphabetical order, and give them those arguments:
+ *
+ *   command string, text
+ *   command node string, text
+ *   schemaname, text, can be null
+ *   objectname, text
+ *
+ * we rebuild the DDL command we're about to execute from the parsetree.
+ *
+ * The queryString comes from untrusted places: it could be a multiple
+ * queries string that has been passed through psql -c or otherwise in the
+ * protocol, or something that comes from an EXECUTE evaluation in plpgsql.
+ *
+ * Also we need to be able to spit out a normalized (canonical?) SQL
+ * command to ease DDL trigger code, and we even provide them with a
+ * nodeToString() output.
+ *
+ */
+
+static RegProcedure *
+list_triggers_for_command(const char *command, char type)
+{
+	int  count = 0, size = 10;
+	RegProcedure *procs = (RegProcedure *) palloc(size*sizeof(RegProcedure));
+
+	Relation	rel, irel;
+	SysScanDesc scandesc;
+	HeapTuple	tuple;
+	ScanKeyData entry[1];
+
+	/* init the first entry of the procs array */
+	procs[0] = InvalidOid;
+
+	rel = heap_open(CmdTriggerRelationId, AccessShareLock);
+	irel = index_open(CmdTriggerCommandNameIndexId, AccessShareLock);
+
+	ScanKeyInit(&entry[0],
+				Anum_pg_cmdtrigger_ctgcommand,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(command));
+
+	scandesc = systable_beginscan_ordered(rel, irel, SnapshotNow, 1, entry);
+
+	while (HeapTupleIsValid(tuple = systable_getnext_ordered(scandesc, ForwardScanDirection)))
+	{
+		Form_pg_cmdtrigger cmd = (Form_pg_cmdtrigger) GETSTRUCT(tuple);
+
+		if (cmd->ctgtype == type)
+		{
+			if (count == size)
+			{
+				size += 10;
+				procs = (Oid *)repalloc(procs, size);
+			}
+			procs[count++] = cmd->ctgfoid;
+			procs[count] = InvalidOid;
+		}
+	}
+	systable_endscan_ordered(scandesc);
+
+	index_close(irel, AccessShareLock);
+	heap_close(rel, AccessShareLock);
+
+	return procs;
+}
+
+static bool
+call_cmdtrigger_procedure(RegProcedure proc, CommandContext cmd,
+						  MemoryContext per_command_context)
+{
+	FmgrInfo	flinfo;
+	FunctionCallInfoData fcinfo;
+	PgStat_FunctionCallUsage fcusage;
+	Datum		result;
+
+	fmgr_info_cxt(proc, &flinfo, per_command_context);
+
+	/* Can't use OidFunctionCallN because we might get a NULL result */
+	InitFunctionCallInfoData(fcinfo, &flinfo, 4, InvalidOid, NULL, NULL);
+
+	fcinfo.arg[0] = PointerGetDatum(cstring_to_text(pstrdup(cmd->cmdstr)));
+
+	if (cmd->nodestr != NULL)
+		fcinfo.arg[1] = PointerGetDatum(cstring_to_text(pstrdup(cmd->nodestr)));
+
+	if (cmd->schemaname != NULL)
+		fcinfo.arg[2] = PointerGetDatum(cstring_to_text(pstrdup(cmd->schemaname)));
+
+	fcinfo.arg[3] = PointerGetDatum(cstring_to_text(pstrdup(cmd->objectname)));
+
+	fcinfo.argnull[0] = false;
+	fcinfo.argnull[1] = cmd->nodestr == NULL;
+	fcinfo.argnull[2] = cmd->schemaname == NULL;
+	fcinfo.argnull[3] = false;
+
+	pgstat_init_function_usage(&fcinfo, &fcusage);
+
+	result = FunctionCallInvoke(&fcinfo);
+
+	pgstat_end_function_usage(&fcusage, true);
+
+	if (!fcinfo.isnull && DatumGetBool(result) == false)
+		return false;
+	return true;
+}
+
+/*
+ * For any given command tag, you can have either Before and After triggers, or
+ * Instead Of triggers, not both.
+ *
+ * Instead Of triggers have to run before the command and to cancel its
+ * execution , hence this API where we return the number of InsteadOf trigger
+ * procedures we fired.
+ */
+int
+ExecBeforeOrInsteadOfCommandTriggers(Node *parsetree, const char *cmdtag)
+{
+	MemoryContext per_command_context;
+	int nb = 0;
+
+	per_command_context =
+		AllocSetContextCreate(CurrentMemoryContext,
+							  "BeforeOrInsteadOfTriggerCommandContext",
+							  ALLOCSET_DEFAULT_MINSIZE,
+							  ALLOCSET_DEFAULT_INITSIZE,
+							  ALLOCSET_DEFAULT_MAXSIZE);
+
+	/*
+	 * You can't have both BEFORE and INSTEAD OF triggers registered on the
+	 * same command, so this function is not checking about that and just going
+	 * through an empty list in at least one of those cases.  The cost of doing
+	 * it this lazy way is an index scan on pg_catalog.pg_cmdtrigger.
+	 */
+	if (!ExecBeforeCommandTriggers(parsetree, cmdtag, per_command_context))
+		nb++;
+	nb += ExecInsteadOfCommandTriggers(parsetree, cmdtag, per_command_context);
+
+	/* Release working resources */
+	MemoryContextDelete(per_command_context);
+	return nb;
+}
+
+bool
+ExecBeforeCommandTriggers(Node *parsetree, const char *cmdtag,
+						  MemoryContext per_command_context)
+{
+	MemoryContext oldContext;
+	CommandContextData cmd;
+	RegProcedure *procs = list_triggers_for_command(cmdtag,
+													CMD_TRIGGER_FIRED_BEFORE);
+	RegProcedure proc;
+	int cur= 0;
+	bool cont = true;
+
+	/*
+	 * Do the functions evaluation in a per-command memory context, so that
+	 * leaked memory will be reclaimed once per command.
+	 */
+	oldContext = MemoryContextSwitchTo(per_command_context);
+	MemoryContextReset(per_command_context);
+
+	while (cont && InvalidOid != (proc = procs[cur++]))
+	{
+		if (cur==1)
+		{
+			cmd.tag = (char *)cmdtag;
+			pg_get_cmddef(&cmd, parsetree);
+		}
+		cont = call_cmdtrigger_procedure(proc, &cmd, per_command_context);
+
+		if (cont == false)
+			elog(WARNING,
+				 "command \"%s %s...\" was cancelled by procedure \"%s\"",
+				 cmdtag, cmd.objectname, get_func_name(proc));
+	}
+	MemoryContextSwitchTo(oldContext);
+	return cont;
+}
+
+/*
+ * return the count of triggers we fired
+ */
+int
+ExecInsteadOfCommandTriggers(Node *parsetree, const char *cmdtag,
+							 MemoryContext per_command_context)
+{
+	MemoryContext oldContext;
+	CommandContextData cmd;
+	RegProcedure *procs = list_triggers_for_command(cmdtag,
+													CMD_TRIGGER_FIRED_INSTEAD);
+	RegProcedure proc;
+	int cur = 0;
+
+	/*
+	 * Do the functions evaluation in a per-command memory context, so that
+	 * leaked memory will be reclaimed once per command.
+	 */
+	oldContext = MemoryContextSwitchTo(per_command_context);
+	MemoryContextReset(per_command_context);
+
+	while (InvalidOid != (proc = procs[cur++]))
+	{
+		if (cur==1)
+		{
+			cmd.tag = (char *)cmdtag;
+			pg_get_cmddef(&cmd, parsetree);
+		}
+		call_cmdtrigger_procedure(proc, &cmd, per_command_context);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+	return cur-1;
+}
+
+void
+ExecAfterCommandTriggers(Node *parsetree, const char *cmdtag)
+{
+	MemoryContext oldContext, per_command_context;
+	CommandContextData cmd;
+	RegProcedure *procs = list_triggers_for_command(cmdtag,
+													CMD_TRIGGER_FIRED_AFTER);
+	RegProcedure proc;
+	int cur = 0;
+
+	/*
+	 * Do the functions evaluation in a per-command memory context, so that
+	 * leaked memory will be reclaimed once per command.
+	 */
+	per_command_context =
+		AllocSetContextCreate(CurrentMemoryContext,
+							  "AfterTriggerCommandContext",
+							  ALLOCSET_DEFAULT_MINSIZE,
+							  ALLOCSET_DEFAULT_INITSIZE,
+							  ALLOCSET_DEFAULT_MAXSIZE);
+
+	oldContext = MemoryContextSwitchTo(per_command_context);
+
+	while (InvalidOid != (proc = procs[cur++]))
+	{
+		if (cur==1)
+		{
+			cmd.tag = (char *)cmdtag;
+			pg_get_cmddef(&cmd, parsetree);
+		}
+		call_cmdtrigger_procedure(proc, &cmd, per_command_context);
+	}
+
+	/* Release working resources */
+	MemoryContextSwitchTo(oldContext);
+	MemoryContextDelete(per_command_context);
+
+	return;
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 63958c3..045282c 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3527,6 +3527,32 @@ _copyDropPropertyStmt(DropPropertyStmt *from)
 	return newnode;
 }
 
+static CreateCmdTrigStmt *
+_copyCreateCmdTrigStmt(CreateCmdTrigStmt *from)
+{
+	CreateCmdTrigStmt *newnode = makeNode(CreateCmdTrigStmt);
+
+	COPY_STRING_FIELD(command);
+	COPY_STRING_FIELD(trigname);
+	COPY_SCALAR_FIELD(timing);
+	COPY_NODE_FIELD(funcname);
+
+	return newnode;
+}
+
+static DropCmdTrigStmt *
+_copyDropCmdTrigStmt(DropCmdTrigStmt *from)
+{
+	DropCmdTrigStmt *newnode = makeNode(DropCmdTrigStmt);
+
+	COPY_STRING_FIELD(command);
+	COPY_STRING_FIELD(trigname);
+	COPY_SCALAR_FIELD(behavior);
+	COPY_SCALAR_FIELD(missing_ok);
+
+	return newnode;
+}
+
 static CreatePLangStmt *
 _copyCreatePLangStmt(CreatePLangStmt *from)
 {
@@ -4422,6 +4448,12 @@ copyObject(void *from)
 		case T_DropPropertyStmt:
 			retval = _copyDropPropertyStmt(from);
 			break;
+		case T_CreateCmdTrigStmt:
+			retval = _copyCreateCmdTrigStmt(from);
+			break;
+		case T_DropCmdTrigStmt:
+			retval = _copyDropCmdTrigStmt(from);
+			break;
 		case T_CreatePLangStmt:
 			retval = _copyCreatePLangStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f3a34a1..c85b5a7 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1837,6 +1837,28 @@ _equalDropPropertyStmt(DropPropertyStmt *a, DropPropertyStmt *b)
 }
 
 static bool
+_equalCreateCmdTrigStmt(CreateCmdTrigStmt *a, CreateCmdTrigStmt *b)
+{
+	COMPARE_STRING_FIELD(command);
+	COMPARE_STRING_FIELD(trigname);
+	COMPARE_SCALAR_FIELD(timing);
+	COMPARE_NODE_FIELD(funcname);
+
+	return true;
+}
+
+static bool
+_equalDropCmdTrigStmt(DropCmdTrigStmt *a, DropCmdTrigStmt *b)
+{
+	COMPARE_STRING_FIELD(command);
+	COMPARE_STRING_FIELD(trigname);
+	COMPARE_SCALAR_FIELD(behavior);
+	COMPARE_SCALAR_FIELD(missing_ok);
+
+	return true;
+}
+
+static bool
 _equalCreatePLangStmt(CreatePLangStmt *a, CreatePLangStmt *b)
 {
 	COMPARE_SCALAR_FIELD(replace);
@@ -2949,6 +2971,12 @@ equal(void *a, void *b)
 		case T_DropPropertyStmt:
 			retval = _equalDropPropertyStmt(a, b);
 			break;
+		case T_CreateCmdTrigStmt:
+			retval = _equalCreateCmdTrigStmt(a, b);
+			break;
+		case T_DropCmdTrigStmt:
+			retval = _equalDropCmdTrigStmt(a, b);
+			break;
 		case T_CreatePLangStmt:
 			retval = _equalCreatePLangStmt(a, b);
 			break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index f7d39ed..7bd3498 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1943,6 +1943,137 @@ _outPlannerParamItem(StringInfo str, PlannerParamItem *node)
  *****************************************************************************/
 
 static void
+_outInsertStmt(StringInfo str, InsertStmt *node)
+{
+	WRITE_NODE_TYPE("INSERTSTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(cols);
+	WRITE_NODE_FIELD(selectStmt);
+	WRITE_NODE_FIELD(returningList);
+	WRITE_NODE_FIELD(withClause);
+}
+
+static void
+_outDeleteStmt(StringInfo str, DeleteStmt *node)
+{
+	WRITE_NODE_TYPE("DELETESTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(usingClause);
+	WRITE_NODE_FIELD(whereClause);
+	WRITE_NODE_FIELD(returningList);
+	WRITE_NODE_FIELD(withClause);
+}
+
+static void
+_outUpdateStmt(StringInfo str, UpdateStmt *node)
+{
+	WRITE_NODE_TYPE("UPDATESTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(targetList);
+	WRITE_NODE_FIELD(whereClause);
+	WRITE_NODE_FIELD(fromClause);
+	WRITE_NODE_FIELD(returningList);
+	WRITE_NODE_FIELD(withClause);
+}
+
+static void
+_outAlterTableStmt(StringInfo str, AlterTableStmt *node)
+{
+	WRITE_NODE_TYPE("ALTERTABLESTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(cmds);
+	WRITE_CHAR_FIELD(relkind);
+}
+
+static void
+_outAlterTableCmd(StringInfo str, AlterTableCmd *node)
+{
+	WRITE_NODE_TYPE("ALTERTABLECMD");
+
+	WRITE_ENUM_FIELD(subtype, AlterTableType);
+	WRITE_STRING_FIELD(name);
+	WRITE_NODE_FIELD(def);
+	WRITE_ENUM_FIELD(behavior, DropBehavior);
+	WRITE_BOOL_FIELD(missing_ok);
+}
+
+static void
+_outAlterDomainStmt(StringInfo str, AlterDomainStmt *node)
+{
+	WRITE_NODE_TYPE("ALTERDOMAINSTMT");
+
+	WRITE_CHAR_FIELD(subtype);
+	WRITE_NODE_FIELD(typeName);
+	WRITE_STRING_FIELD(name);
+	WRITE_NODE_FIELD(def);
+	WRITE_ENUM_FIELD(behavior, DropBehavior);
+}
+
+static void
+_outGrantStmt(StringInfo str, GrantStmt *node)
+{
+	WRITE_NODE_TYPE("GRANTSTMT");
+
+	WRITE_BOOL_FIELD(is_grant);
+	WRITE_ENUM_FIELD(targtype, GrantTargetType);
+	WRITE_ENUM_FIELD(objtype, GrantObjectType);
+	WRITE_NODE_FIELD(objects);
+	WRITE_NODE_FIELD(privileges);
+	WRITE_NODE_FIELD(grantees);
+	WRITE_BOOL_FIELD(grant_option);
+	WRITE_ENUM_FIELD(behavior, DropBehavior);
+}
+
+static void
+_outGrantRoleStmt(StringInfo str, GrantRoleStmt *node)
+{
+	WRITE_NODE_TYPE("GRANTROLESTMT");
+
+	WRITE_NODE_FIELD(granted_roles);
+	WRITE_NODE_FIELD(grantee_roles);
+	WRITE_BOOL_FIELD(is_grant);
+	WRITE_BOOL_FIELD(admin_opt);
+	WRITE_STRING_FIELD(grantor);
+	WRITE_ENUM_FIELD(behavior, DropBehavior);
+}
+
+static void
+_outAlterDefaultPrivilegesStmt(StringInfo str, AlterDefaultPrivilegesStmt *node)
+{
+	WRITE_NODE_TYPE("ALTERDEFAULTPRIVILEGESSTMT");
+
+	WRITE_NODE_FIELD(options);
+	WRITE_NODE_FIELD(action);
+}
+
+static void
+_outClusterStmt(StringInfo str, ClusterStmt *node)
+{
+	WRITE_NODE_TYPE("CLUSTERSTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_STRING_FIELD(indexname);
+	WRITE_BOOL_FIELD(verbose);
+}
+
+static void
+_outCopyStmt(StringInfo str, CopyStmt *node)
+{
+	WRITE_NODE_TYPE("COPYSTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(query);
+	WRITE_NODE_FIELD(attlist);
+	WRITE_BOOL_FIELD(is_from);
+	WRITE_STRING_FIELD(filename);
+	WRITE_NODE_FIELD(options);
+}
+
+static void
 _outCreateStmt(StringInfo str, CreateStmt *node)
 {
 	WRITE_NODE_TYPE("CREATESTMT");
@@ -1959,6 +2090,31 @@ _outCreateStmt(StringInfo str, CreateStmt *node)
 }
 
 static void
+_outDefineStmt(StringInfo str, DefineStmt *node)
+{
+	WRITE_NODE_TYPE("DEFINESTMT");
+
+	WRITE_ENUM_FIELD(kind, ObjectType);
+	WRITE_BOOL_FIELD(oldstyle);
+	WRITE_NODE_FIELD(defnames);
+	WRITE_NODE_FIELD(args);
+	WRITE_NODE_FIELD(definition);
+}
+
+static void
+_outAlterTSConfigurationStmt(StringInfo str, AlterTSConfigurationStmt *node)
+{
+	WRITE_NODE_TYPE("ALTERTSCONFIGURATIONSTMT");
+
+	WRITE_NODE_FIELD(cfgname);
+	WRITE_NODE_FIELD(tokentype);
+	WRITE_NODE_FIELD(dicts);
+	WRITE_BOOL_FIELD(override);
+	WRITE_BOOL_FIELD(replace);
+	WRITE_BOOL_FIELD(missing_ok);
+}
+
+static void
 _outCreateForeignTableStmt(StringInfo str, CreateForeignTableStmt *node)
 {
 	WRITE_NODE_TYPE("CREATEFOREIGNTABLESTMT");
@@ -1970,6 +2126,17 @@ _outCreateForeignTableStmt(StringInfo str, CreateForeignTableStmt *node)
 }
 
 static void
+_outCommentStmt(StringInfo str, CommentStmt *node)
+{
+	WRITE_NODE_TYPE("COMMENTSTMT");
+
+	WRITE_ENUM_FIELD(objtype, ObjectType);
+	WRITE_NODE_FIELD(objname);
+	WRITE_NODE_FIELD(objargs);
+	WRITE_STRING_FIELD(comment);
+}
+
+static void
 _outIndexStmt(StringInfo str, IndexStmt *node)
 {
 	WRITE_NODE_TYPE("INDEXSTMT");
@@ -1993,6 +2160,33 @@ _outIndexStmt(StringInfo str, IndexStmt *node)
 }
 
 static void
+_outCreateFunctionStmt(StringInfo str, CreateFunctionStmt *node)
+{
+	WRITE_NODE_TYPE("CREATEFUNCTIONSTMT");
+
+	WRITE_BOOL_FIELD(replace);
+	WRITE_NODE_FIELD(funcname);
+	WRITE_NODE_FIELD(parameters);
+	WRITE_NODE_FIELD(returnType);
+	WRITE_NODE_FIELD(options);
+	WRITE_NODE_FIELD(withClause);
+}
+
+static void
+_outRuleStmt(StringInfo str, RuleStmt *node)
+{
+	WRITE_NODE_TYPE("RULESTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_STRING_FIELD(rulename);
+	WRITE_NODE_FIELD(whereClause);
+	WRITE_ENUM_FIELD(event, CmdType);
+	WRITE_BOOL_FIELD(instead);
+	WRITE_NODE_FIELD(actions);
+	WRITE_BOOL_FIELD(replace);
+}
+
+static void
 _outNotifyStmt(StringInfo str, NotifyStmt *node)
 {
 	WRITE_NODE_TYPE("NOTIFY");
@@ -2002,6 +2196,95 @@ _outNotifyStmt(StringInfo str, NotifyStmt *node)
 }
 
 static void
+_outViewStmt(StringInfo str, ViewStmt *node)
+{
+	WRITE_NODE_TYPE("VIEWSTMT");
+
+	WRITE_NODE_FIELD(view);
+	WRITE_NODE_FIELD(aliases);
+	WRITE_NODE_FIELD(query);
+	WRITE_BOOL_FIELD(replace);
+}
+
+static void
+_outCreateDomainStmt(StringInfo str, CreateDomainStmt *node)
+{
+	WRITE_NODE_TYPE("CREATECONVERSIONSTMT");
+
+	WRITE_NODE_FIELD(domainname);
+	WRITE_NODE_FIELD(typeName);
+	WRITE_NODE_FIELD(collClause);
+	WRITE_NODE_FIELD(constraints);
+}
+
+static void
+_outCreatedbStmt(StringInfo str, CreatedbStmt *node)
+{
+	WRITE_NODE_TYPE("CREATEDBSTMT");
+
+	WRITE_STRING_FIELD(dbname);
+	WRITE_NODE_FIELD(options);
+}
+
+static void
+_outVacuumStmt(StringInfo str, VacuumStmt *node)
+{
+	WRITE_NODE_TYPE("VACUUMSTMT");
+
+	WRITE_INT_FIELD(options);
+	WRITE_INT_FIELD(freeze_min_age);
+	WRITE_INT_FIELD(freeze_table_age);
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(va_cols);
+}
+
+static void
+_outVariableSetStmt(StringInfo str, VariableSetStmt *node)
+{
+	WRITE_NODE_TYPE("VARIABLESETSTMT");
+
+	WRITE_ENUM_FIELD(kind, VariableSetKind);
+	WRITE_STRING_FIELD(name);
+	WRITE_NODE_FIELD(args);
+	WRITE_BOOL_FIELD(is_local);
+}
+
+static void
+_outCreatePLangStmt(StringInfo str, CreatePLangStmt *node)
+{
+	WRITE_NODE_TYPE("CREATESPLANGSTMT");
+
+	WRITE_BOOL_FIELD(replace);
+	WRITE_STRING_FIELD(plname);
+	WRITE_NODE_FIELD(plhandler);
+	WRITE_NODE_FIELD(plinline);
+	WRITE_NODE_FIELD(plvalidator);
+	WRITE_BOOL_FIELD(pltrusted);
+}
+
+static void
+_outCreateSchemaStmt(StringInfo str, CreateSchemaStmt *node)
+{
+	WRITE_NODE_TYPE("CREATESCHEMASTMT");
+
+	WRITE_STRING_FIELD(schemaname);
+	WRITE_STRING_FIELD(authid);
+	WRITE_NODE_FIELD(schemaElts);
+}
+
+static void
+_outCreateConversionStmt(StringInfo str, CreateConversionStmt *node)
+{
+	WRITE_NODE_TYPE("CREATECONVERSIONSTMT");
+
+	WRITE_NODE_FIELD(conversion_name);
+	WRITE_STRING_FIELD(for_encoding_name);
+	WRITE_STRING_FIELD(to_encoding_name);
+	WRITE_NODE_FIELD(func_name);
+	WRITE_BOOL_FIELD(def);
+}
+
+static void
 _outDeclareCursorStmt(StringInfo str, DeclareCursorStmt *node)
 {
 	WRITE_NODE_TYPE("DECLARECURSOR");
@@ -2063,6 +2346,23 @@ _outDefElem(StringInfo str, DefElem *node)
 }
 
 static void
+_outPrivGrantee(StringInfo str, PrivGrantee *node)
+{
+	WRITE_NODE_TYPE("PRIVGRANTEE");
+
+	WRITE_STRING_FIELD(rolname);
+}
+
+static void
+_outAccessPriv(StringInfo str, AccessPriv *node)
+{
+	WRITE_NODE_TYPE("ACCESSPRIV");
+
+	WRITE_STRING_FIELD(priv_name);
+	WRITE_NODE_FIELD(cols);
+}
+
+static void
 _outInhRelation(StringInfo str, InhRelation *node)
 {
 	WRITE_NODE_TYPE("INHRELATION");
@@ -2093,6 +2393,16 @@ _outXmlSerialize(StringInfo str, XmlSerialize *node)
 }
 
 static void
+_outCreateExtensionStmt(StringInfo str, CreateExtensionStmt *node)
+{
+	WRITE_NODE_TYPE("CREATEEXTENSIONSTMT");
+
+	WRITE_STRING_FIELD(extname);
+	WRITE_BOOL_FIELD(if_not_exists);
+	WRITE_NODE_FIELD(options);
+}
+
+static void
 _outColumnDef(StringInfo str, ColumnDef *node)
 {
 	WRITE_NODE_TYPE("COLUMNDEF");
@@ -2250,6 +2560,17 @@ _outWindowClause(StringInfo str, WindowClause *node)
 }
 
 static void
+_outFunctionParameter(StringInfo str, FunctionParameter *node)
+{
+	WRITE_NODE_TYPE("FUNCTIONPARAMETER");
+
+	WRITE_STRING_FIELD(name);
+	WRITE_NODE_FIELD(argType);
+	WRITE_CHAR_FIELD(mode);
+	WRITE_NODE_FIELD(defexpr);
+}
+
+static void
 _outRowMarkClause(StringInfo str, RowMarkClause *node)
 {
 	WRITE_NODE_TYPE("ROWMARKCLAUSE");
@@ -3033,24 +3354,99 @@ _outNode(StringInfo str, void *obj)
 				_outPlannerParamItem(str, obj);
 				break;
 
+			case T_InsertStmt:
+				_outInsertStmt(str, obj);
+				break;
+			case T_DeleteStmt:
+				_outDeleteStmt(str, obj);
+				break;
+			case T_UpdateStmt:
+				_outUpdateStmt(str, obj);
+				break;
+			case T_AlterTableStmt:
+				_outAlterTableStmt(str, obj);
+				break;
+			case T_AlterTableCmd:
+				_outAlterTableCmd(str, obj);
+				break;
+			case T_AlterDomainStmt:
+				_outAlterDomainStmt(str, obj);
+				break;
+			case T_GrantStmt:
+				_outGrantStmt(str, obj);
+				break;
+			case T_GrantRoleStmt:
+				_outGrantRoleStmt(str, obj);
+				break;
+			case T_AlterDefaultPrivilegesStmt:
+				_outAlterDefaultPrivilegesStmt(str, obj);
+				break;
+			case T_ClusterStmt:
+				_outClusterStmt(str, obj);
+				break;
+			case T_CopyStmt:
+				_outCopyStmt(str, obj);
+				break;
 			case T_CreateStmt:
 				_outCreateStmt(str, obj);
 				break;
+			case T_DefineStmt:
+				_outDefineStmt(str, obj);
+				break;
+			case T_AlterTSConfigurationStmt:
+				_outAlterTSConfigurationStmt(str, obj);
+				break;
 			case T_CreateForeignTableStmt:
 				_outCreateForeignTableStmt(str, obj);
 				break;
+			case T_CommentStmt:
+				_outCommentStmt(str, obj);
+				break;
 			case T_IndexStmt:
 				_outIndexStmt(str, obj);
 				break;
+			case T_CreateFunctionStmt:
+				_outCreateFunctionStmt(str, obj);
+				break;
+			case T_RuleStmt:
+				_outRuleStmt(str, obj);
+				break;
 			case T_NotifyStmt:
 				_outNotifyStmt(str, obj);
 				break;
+			case T_ViewStmt:
+				_outViewStmt(str, obj);
+				break;
+			case T_CreateDomainStmt:
+				_outCreateDomainStmt(str, obj);
+				break;
+			case T_CreatedbStmt:
+				_outCreatedbStmt(str, obj);
+				break;
+			case T_VacuumStmt:
+				_outVacuumStmt(str, obj);
+				break;
+			case T_VariableSetStmt:
+				_outVariableSetStmt(str, obj);
+				break;
+			case T_CreatePLangStmt:
+				_outCreatePLangStmt(str, obj);
+				break;
+			case T_CreateSchemaStmt:
+				_outCreateSchemaStmt(str, obj);
+				break;
+			case T_CreateConversionStmt:
+				_outCreateConversionStmt(str, obj);
+				break;
 			case T_DeclareCursorStmt:
 				_outDeclareCursorStmt(str, obj);
 				break;
 			case T_SelectStmt:
 				_outSelectStmt(str, obj);
 				break;
+			case T_CreateExtensionStmt:
+				_outCreateExtensionStmt(str, obj);
+				break;
 			case T_ColumnDef:
 				_outColumnDef(str, obj);
 				break;
@@ -3075,6 +3471,9 @@ _outNode(StringInfo str, void *obj)
 			case T_WindowClause:
 				_outWindowClause(str, obj);
 				break;
+			case T_FunctionParameter:
+				_outFunctionParameter(str, obj);
+				break;
 			case T_RowMarkClause:
 				_outRowMarkClause(str, obj);
 				break;
@@ -3138,6 +3537,12 @@ _outNode(StringInfo str, void *obj)
 			case T_DefElem:
 				_outDefElem(str, obj);
 				break;
+			case T_PrivGrantee:
+				_outPrivGrantee(str, obj);
+				break;
+			case T_AccessPriv:
+				_outAccessPriv(str, obj);
+				break;
 			case T_InhRelation:
 				_outInhRelation(str, obj);
 				break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c135465..052791d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -208,12 +208,12 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
 		CreateSchemaStmt CreateSeqStmt CreateStmt CreateTableSpaceStmt
 		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
-		CreateAssertStmt CreateTrigStmt
+		CreateAssertStmt CreateTrigStmt CreateCmdTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
 		DropGroupStmt DropOpClassStmt DropOpFamilyStmt DropPLangStmt DropStmt
-		DropAssertStmt DropTrigStmt DropRuleStmt DropCastStmt DropRoleStmt
-		DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt
+		DropAssertStmt DropTrigStmt DropCmdTrigStmt DropRuleStmt DropCastStmt
+		DropRoleStmt DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt
 		DropForeignServerStmt DropUserMappingStmt ExplainStmt FetchStmt
 		GrantStmt GrantRoleStmt IndexStmt InsertStmt ListenStmt LoadStmt
 		LockStmt NotifyStmt ExplainableStmt PreparableStmt
@@ -268,6 +268,7 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 %type <list>	TriggerEvents TriggerOneEvent
 %type <value>	TriggerFuncArg
 %type <node>	TriggerWhen
+%type <str>		trigger_command
 
 %type <str>		copy_file_name
 				database_name access_method_clause access_method attr_name
@@ -496,7 +497,7 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 
 	CACHE CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
 	CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
-	CLUSTER COALESCE COLLATE COLLATION COLUMN COMMENT COMMENTS COMMIT
+	CLUSTER COALESCE COLLATE COLLATION COLUMN COMMAND COMMENT COMMENTS COMMIT
 	COMMITTED CONCURRENTLY CONFIGURATION CONNECTION CONSTRAINT CONSTRAINTS
 	CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE
 	CROSS CSV CURRENT_P
@@ -727,6 +728,7 @@ stmt :
 			| CreateStmt
 			| CreateTableSpaceStmt
 			| CreateTrigStmt
+			| CreateCmdTrigStmt
 			| CreateRoleStmt
 			| CreateUserStmt
 			| CreateUserMappingStmt
@@ -750,6 +752,7 @@ stmt :
 			| DropStmt
 			| DropTableSpaceStmt
 			| DropTrigStmt
+			| DropCmdTrigStmt
 			| DropRoleStmt
 			| DropUserStmt
 			| DropUserMappingStmt
@@ -4219,6 +4222,65 @@ DropTrigStmt:
 /*****************************************************************************
  *
  *		QUERIES :
+ *				CREATE TRIGGER ... BEFORE|INSTEAD OF|AFTER COMMAND ...
+ *				DROP TRIGGER ... ON COMMAND ...
+ *
+ *****************************************************************************/
+
+CreateCmdTrigStmt:
+			CREATE TRIGGER name TriggerActionTime COMMAND trigger_command
+			EXECUTE PROCEDURE func_name '(' TriggerFuncArgs ')'
+				{
+					CreateCmdTrigStmt *n = makeNode(CreateCmdTrigStmt);
+					n->trigname = $3;
+					n->timing   = $4;
+					n->command  = $6;
+					n->funcname = $9;
+					$$ = (Node *)n;
+				}
+		;
+
+/*
+ * that will get matched against what CreateCommandTag  returns
+ *
+ * we don't support Command Triggers on every possible command that PostgreSQL
+ * supports, this list should match with the implementation of rewriting
+ * utility statements in pg_get_cmddef() in src/backend/utils/adt/ruleutils.c
+ */
+trigger_command:
+			CREATE TABLE						{ $$ = "CREATE TABLE"; }
+			| ALTER TABLE						{ $$ = "ALTER TABLE"; }
+			| DROP TABLE						{ $$ = "DROP TABLE"; }
+			| CREATE VIEW						{ $$ = "CREATE VIEW"; }
+			| DROP VIEW							{ $$ = "DROP VIEW"; }
+			| CREATE EXTENSION					{ $$ = "CREATE EXTENSION"; }
+			| DROP EXTENSION					{ $$ = "DROP EXTENSION"; }
+		;
+
+DropCmdTrigStmt:
+			DROP TRIGGER name ON COMMAND trigger_command opt_drop_behavior
+				{
+					DropCmdTrigStmt *n = makeNode(DropCmdTrigStmt);
+					n->trigname = $3;
+					n->command  = $6;
+					n->behavior = $7;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| DROP TRIGGER IF_P EXISTS name ON COMMAND trigger_command opt_drop_behavior
+				{
+					DropCmdTrigStmt *n = makeNode(DropCmdTrigStmt);
+					n->trigname = $5;
+					n->command  = $8;
+					n->behavior = $9;
+					n->missing_ok = true;
+					$$ = (Node *) n;
+				}
+		;
+
+/*****************************************************************************
+ *
+ *		QUERIES :
  *				CREATE ASSERTION ...
  *				DROP ASSERTION ...
  *
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 5b06333..ec863c9 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -25,6 +25,7 @@
 #include "commands/alter.h"
 #include "commands/async.h"
 #include "commands/cluster.h"
+#include "commands/cmdtrigger.h"
 #include "commands/comment.h"
 #include "commands/collationcmds.h"
 #include "commands/conversioncmds.h"
@@ -184,6 +185,7 @@ check_xact_readonly(Node *parsetree)
 		case T_CommentStmt:
 		case T_DefineStmt:
 		case T_CreateCastStmt:
+		case T_CreateCmdTrigStmt:
 		case T_CreateConversionStmt:
 		case T_CreatedbStmt:
 		case T_CreateDomainStmt:
@@ -329,8 +331,17 @@ ProcessUtility(Node *parsetree,
 			   DestReceiver *dest,
 			   char *completionTag)
 {
+	/* we want a completion tag to identify which triggers to run, and that's
+	 * true whatever is given as completionTag here, so just call
+	 * CreateCommandTag() for our own business.
+	 */
+	const char *commandTag = CreateCommandTag(parsetree);
+
 	Assert(queryString != NULL);	/* required as of 8.4 */
 
+	if (ExecBeforeOrInsteadOfCommandTriggers(parsetree, commandTag) > 0)
+		return;
+
 	/*
 	 * We provide a function hook variable that lets loadable plugins get
 	 * control when ProcessUtility is called.  Such a plugin would normally
@@ -342,6 +353,8 @@ ProcessUtility(Node *parsetree,
 	else
 		standard_ProcessUtility(parsetree, queryString, params,
 								isTopLevel, dest, completionTag);
+
+	ExecAfterCommandTriggers(parsetree, commandTag);
 }
 
 void
@@ -1103,6 +1116,14 @@ standard_ProcessUtility(Node *parsetree,
 			}
 			break;
 
+		case T_CreateCmdTrigStmt:
+			(void) CreateCmdTrigger((CreateCmdTrigStmt *) parsetree, queryString);
+			break;
+
+		case T_DropCmdTrigStmt:
+			DropCmdTrigger((DropCmdTrigStmt *) parsetree);
+			break;
+
 		case T_CreatePLangStmt:
 			CreateProceduralLanguage((CreatePLangStmt *) parsetree);
 			break;
@@ -2016,6 +2037,14 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
+		case T_CreateCmdTrigStmt:
+			tag = "CREATE COMMAND TRIGGER";
+			break;
+
+		case T_DropCmdTrigStmt:
+			tag = "DROP COMMAND TRIGGER";
+			break;
+
 		case T_CreatePLangStmt:
 			tag = "CREATE LANGUAGE";
 			break;
@@ -2233,6 +2262,13 @@ CreateCommandTag(Node *parsetree)
 			break;
 	}
 
+	/*
+	 * Useful to raise WARNINGs for any DDL command not yet supported.
+	 *
+	elog(WARNING, "Command Tag:    %s", tag);
+	elog(WARNING, "Note to String: %s", nodeToString(parsetree));
+	 */
+
 	return tag;
 }
 
@@ -2537,6 +2573,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_CreateCmdTrigStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_DropCmdTrigStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		case T_CreatePLangStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 75923a6..5324882 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -39,9 +39,11 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/clauses.h"
 #include "optimizer/tlist.h"
+#include "parser/analyze.h"
 #include "parser/keywords.h"
 #include "parser/parse_func.h"
 #include "parser/parse_oper.h"
+#include "parser/parse_type.h"
 #include "parser/parser.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteHandler.h"
@@ -256,7 +258,6 @@ static char *flatten_reloptions(Oid relid);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
-
 /* ----------
  * get_ruledef			- Do it all and return a text
  *				  that could be used as a statement
@@ -7326,3 +7327,613 @@ flatten_reloptions(Oid relid)
 
 	return result;
 }
+
+/*
+ * Functions that ouputs a COMMAND given a Utility parsetree
+ *
+ * FIXME: First some tools that I couldn't find in the sources.
+ */
+static char *
+RangeVarToString(RangeVar *r)
+{
+	char *qualified_name = (char *)palloc0(NAMEDATALEN*2+2);
+
+	sprintf(qualified_name, "%s%s%s",
+			r->schemaname == NULL? "": r->schemaname,
+			r->schemaname == NULL? "": ".",
+			r->relname);
+
+	return qualified_name;
+}
+
+static const char *
+relkindToString(ObjectType relkind)
+{
+	const char *kind;
+
+	switch (relkind)
+	{
+		case OBJECT_FOREIGN_TABLE:
+			kind = "FOREIGN TABLE";
+			break;
+
+		case OBJECT_INDEX:
+			kind = "INDEX";
+			break;
+
+		case OBJECT_SEQUENCE:
+			kind = "SEQUENCE";
+			break;
+
+		case OBJECT_TABLE:
+			kind = "TABLE";
+			break;
+
+		case OBJECT_VIEW:
+			kind = "VIEW";
+			break;
+
+		default:
+			elog(DEBUG2, "unrecognized relkind: %d", relkind);
+			break;
+	}
+	return kind;
+}
+
+static void
+_maybeAddSeparator(StringInfo buf, const char *sep, bool *first)
+{
+	if (*first) *first = false;
+	else        appendStringInfoString(buf, sep);
+}
+
+/*
+ * The DROP statement is "generic" as in supporting multiple object types. The
+ * specialized part is only finding the names of the objects dropped.
+ *
+ * Also the easiest way to get the command prefix is to use the command tag.
+ */
+static void
+_rwDropStmt(CommandContext cmd, DropStmt *node)
+{
+	StringInfoData buf;
+	ListCell *obj;
+	bool first = true;
+
+	initStringInfo(&buf);
+	appendStringInfo(&buf, "%s ", cmd->tag);
+
+	foreach(obj, node->objects)
+	{
+		switch (node->removeType)
+		{
+			case OBJECT_TABLE:
+			case OBJECT_SEQUENCE:
+			case OBJECT_VIEW:
+			case OBJECT_INDEX:
+			case OBJECT_FOREIGN_TABLE:
+			{
+				RangeVar *rel = makeRangeVarFromNameList((List *) lfirst(obj));
+				_maybeAddSeparator(&buf, ", ", &first);
+				appendStringInfoString(&buf, RangeVarToString(rel));
+
+				cmd->schemaname = rel->schemaname;
+				cmd->objectname = rel->relname;
+				break;
+			}
+
+			case OBJECT_TYPE:
+			case OBJECT_DOMAIN:
+			{
+				TypeName *typename = makeTypeNameFromNameList((List *) obj);
+				_maybeAddSeparator(&buf, ", ", &first);
+				appendStringInfoString(&buf, TypeNameToString(typename));
+
+				if (list_nth((List *) obj, 1) == NIL)
+				{
+					cmd->schemaname = NULL;
+					cmd->objectname = strVal(linitial((List *) obj));
+				}
+				else
+				{
+					cmd->schemaname = strVal(list_nth((List *) obj, 0));
+					cmd->objectname = strVal(list_nth((List *) obj, 1));
+				}
+				break;
+			}
+
+			/* case OBJECT_COLLATION: */
+			/* case OBJECT_CONVERSION: */
+			/* case OBJECT_SCHEMA: */
+			/* case OBJECT_EXTENSION: */
+			default:
+			{
+				char *name = strVal(linitial((List *) obj));
+				_maybeAddSeparator(&buf, ", ", &first);
+				appendStringInfoString(&buf, name);
+
+				cmd->schemaname = NULL;
+				cmd->objectname = name;
+				break;
+			}
+		}
+	}
+	appendStringInfo(&buf, "%s %s;",
+					 node->missing_ok ? " IF EXISTS":"",
+					 node->behavior == DROP_CASCADE ? "CASCADE" : "RESTRICT");
+
+	cmd->cmdstr = buf.data;
+}
+
+static void
+_rwCreateExtensionStmt(CommandContext cmd, CreateExtensionStmt *node)
+{
+	StringInfoData buf;
+	ListCell   *lc;
+
+	initStringInfo(&buf);
+	appendStringInfo(&buf, "CREATE EXTENSION%s %s",
+					 node->if_not_exists ? " IF NOT EXISTS" : "",
+					 node->extname);
+
+	foreach(lc, node->options)
+	{
+		DefElem    *defel = (DefElem *) lfirst(lc);
+
+		if (strcmp(defel->defname, "schema") == 0)
+			appendStringInfo(&buf, " SCHEMA %s", strVal(defel->arg));
+
+		else if (strcmp(defel->defname, "new_version") == 0)
+			appendStringInfo(&buf, " VERSION %s", strVal(defel->arg));
+
+		else if (strcmp(defel->defname, "old_version") == 0)
+			appendStringInfo(&buf, " FROM %s", strVal(defel->arg));
+	}
+	appendStringInfoChar(&buf, ';');
+
+	cmd->cmdstr = buf.data;
+	cmd->schemaname = NULL;
+	cmd->objectname = node->extname;
+}
+
+static void
+_rwViewStmt(CommandContext cmd, ViewStmt *node)
+{
+	StringInfoData buf;
+	Query	   *viewParse;
+
+	initStringInfo(&buf);
+	viewParse = parse_analyze((Node *) copyObject(node->query),
+							  "(unavailable source text)", NULL, 0);
+
+	appendStringInfo(&buf, "CREATE %s %s AS ",
+					 node->replace? "OR REPLACE VIEW": "VIEW",
+					 RangeVarToString(node->view));
+
+	get_query_def(viewParse, &buf, NIL, NULL, 0, 1);
+	appendStringInfoChar(&buf, ';');
+
+	cmd->cmdstr = buf.data;
+	cmd->schemaname = node->view->schemaname;
+	cmd->objectname = node->view->relname;
+}
+
+static void
+_rwColQualList(StringInfo buf, List *constraints, const char *relname)
+{
+	ListCell   *lc;
+
+	foreach(lc, constraints)
+	{
+		Constraint *c = (Constraint *) lfirst(lc);
+
+		if (c->conname != NULL)
+			appendStringInfo(buf, " CONSTRAINT %s", c->conname);
+
+		switch (c->contype)
+		{
+			case CONSTR_NOTNULL:
+				appendStringInfo(buf, " NOT NULL");
+				break;
+
+			case CONSTR_NULL:
+				appendStringInfo(buf, " NULL");
+				break;
+
+			case CONSTR_UNIQUE:
+				appendStringInfo(buf, " UNIQUE");
+				if (c->indexspace != NULL)
+					appendStringInfo(buf, " USING INDEX TABLESPACE %s", c->indexspace);
+				break;
+
+			case CONSTR_PRIMARY:
+				appendStringInfo(buf, " PRIMARY KEY");
+				if (c->keys != NULL)
+				{
+					ListCell *k;
+					bool first = true;
+
+					appendStringInfoChar(buf, '(');
+					foreach(k, c->keys)
+					{
+						_maybeAddSeparator(buf, ",", &first);
+						appendStringInfo(buf, "%s", strVal(lfirst(k)));
+					}
+					appendStringInfoChar(buf, ')');
+				}
+				if (c->indexspace != NULL)
+					appendStringInfo(buf, " USING INDEX TABLESPACE %s", c->indexspace);
+				break;
+
+			case CONSTR_CHECK:
+			{
+				char	   *consrc;
+				List	   *context;
+
+				context = deparse_context_for(relname, InvalidOid);
+				consrc = deparse_expression(c->raw_expr, context, false, false);
+				appendStringInfo(buf, " CHECK (%s)", consrc);
+				break;
+			}
+
+			case CONSTR_DEFAULT:
+			{
+				char	   *consrc;
+				List	   *context;
+
+				context = deparse_context_for(relname, InvalidOid);
+				consrc = deparse_expression(c->raw_expr, context, false, false);
+				appendStringInfo(buf, " DEFAUT %s", consrc);
+				break;
+			}
+
+			case CONSTR_EXCLUSION:
+				appendStringInfo(buf, " EXCLUDE %s () ", c->access_method);
+				if (c->indexspace != NULL)
+					appendStringInfo(buf, " USING INDEX TABLESPACE %s", c->indexspace);
+				break;
+
+			case CONSTR_FOREIGN:
+				appendStringInfo(buf, " REFERENCES %s()", RangeVarToString(c->pktable));
+				break;
+
+			case CONSTR_ATTR_DEFERRABLE:
+				appendStringInfo(buf, " DEFERRABLE");
+				break;
+
+			case CONSTR_ATTR_NOT_DEFERRABLE:
+				appendStringInfo(buf, " NOT DEFERRABLE");
+				break;
+
+			case CONSTR_ATTR_DEFERRED:
+				appendStringInfo(buf, " INITIALLY DEFERRED");
+				break;
+
+			case CONSTR_ATTR_IMMEDIATE:
+				appendStringInfo(buf, " INITIALLY IMMEDIATE");
+				break;
+		}
+	}
+}
+
+static void
+_rwCreateStmt(CommandContext cmd, CreateStmt *node)
+{
+	ListCell   *lcmd;
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+	appendStringInfo(&buf, "CREATE TABLE %s %s",
+					 RangeVarToString(node->relation),
+					 node->if_not_exists ? " IF NOT EXISTS" : "");
+
+	appendStringInfoChar(&buf, '(');
+
+	foreach(lcmd, node->tableElts)
+	{
+		Node *elmt = (Node *) lfirst(lcmd);
+
+		switch (nodeTag(elmt))
+		{
+			case T_ColumnDef:
+			{
+				ColumnDef  *c = (ColumnDef *) elmt;
+				appendStringInfo(&buf, "%s %s",
+								 c->colname,
+								 TypeNameToString(c->typeName));
+				_rwColQualList(&buf, c->constraints, node->relation->relname);
+				break;
+			}
+			case T_InhRelation:
+			{
+				InhRelation  *r = (InhRelation *) elmt;
+				appendStringInfo(&buf, "%s", RangeVarToString(r->relation));
+				break;
+			}
+			case T_Constraint:
+			{
+				Constraint  *c = (Constraint *) elmt;
+				_rwColQualList(&buf, list_make1(c), node->relation->relname);
+				break;
+			}
+			default:
+				/* Many nodeTags are not interesting as an
+				 * OptTableElementList
+				 */
+				break;
+		}
+		if (lnext(lcmd) != NULL)
+			appendStringInfoChar(&buf, ',');
+	}
+	appendStringInfoChar(&buf, ')');
+	appendStringInfoChar(&buf, ';');
+
+	cmd->cmdstr = buf.data;
+	cmd->schemaname = node->relation->schemaname;
+	cmd->objectname = node->relation->relname;
+}
+
+static void
+_rwAlterTableStmt(CommandContext cmd, AlterTableStmt *node)
+{
+	StringInfoData buf;
+	ListCell   *lcmd;
+	bool        first = true;
+
+	initStringInfo(&buf);
+	appendStringInfo(&buf, "ALTER %s %s",
+					 relkindToString(node->relkind),
+					 RangeVarToString(node->relation));
+
+	foreach(lcmd, node->cmds)
+	{
+		AlterTableCmd *cmd = (AlterTableCmd *) lfirst(lcmd);
+		ColumnDef  *def = (ColumnDef *) cmd->def;
+
+		_maybeAddSeparator(&buf, ", ", &first);
+
+		switch (cmd->subtype)
+		{
+			case AT_AddColumn:				/* add column */
+				appendStringInfo(&buf, " ADD COLUMN %s %s",
+								 def->colname,
+								 TypeNameToString(def->typeName));
+
+				if (def->is_not_null)
+					appendStringInfoString(&buf, " NOT NULL");
+				break;
+
+			case AT_ColumnDefault:			/* alter column default */
+				if (def == NULL)
+					appendStringInfo(&buf, " ALTER %s DROP DEFAULT",
+									 cmd->name);
+				else
+				{
+					char *str =
+						deparse_expression_pretty(cmd->def, NIL, false, false, 0, 0);
+
+					appendStringInfo(&buf, " ALTER %s SET DEFAULT %s",
+									 cmd->name, str);
+				}
+				break;
+
+			case AT_DropNotNull:			/* alter column drop not null */
+				appendStringInfo(&buf, " ALTER %s DROP NOT NULL", cmd->name);
+				break;
+
+			case AT_SetNotNull:				/* alter column set not null */
+				appendStringInfo(&buf, " ALTER %s SET NOT NULL", cmd->name);
+				break;
+
+			case AT_SetStatistics:			/* alter column set statistics */
+				appendStringInfo(&buf, " ALTER %s SET STATISTICS %ld",
+								 cmd->name,
+								 (long) intVal((Value *)(cmd->def)));
+				break;
+
+			case AT_SetOptions:				/* alter column set ( options ) */
+				break;
+
+			case AT_ResetOptions:			/* alter column reset ( options ) */
+				break;
+
+			case AT_SetStorage:				/* alter column set storage */
+				appendStringInfo(&buf, " ALTER %s SET STORAGE %s",
+								 cmd->name,
+								 strVal((Value *)(cmd->def)));
+				break;
+
+			case AT_DropColumn:				/* drop column */
+				appendStringInfo(&buf, " %s %s%s",
+								 cmd->missing_ok? "DROP IF EXISTS": "DROP",
+								 cmd->name,
+								 cmd->behavior == DROP_CASCADE? " CASCADE": "");
+				break;
+
+			case AT_AddIndex:				/* add index */
+				break;
+
+			case AT_AddConstraint:			/* add constraint */
+				break;
+
+			case AT_ValidateConstraint:		/* validate constraint */
+				appendStringInfo(&buf, " VALIDATE CONSTRAINT %s", cmd->name);
+				break;
+
+			case AT_AddIndexConstraint:		/* add constraint using existing index */
+				break;
+
+			case AT_DropConstraint:			/* drop constraint */
+				appendStringInfo(&buf, " DROP CONSTRAINT%s %s %s",
+								 cmd->missing_ok? " IF EXISTS": "",
+								 cmd->name,
+								 cmd->behavior == DROP_CASCADE? " CASCADE": "");
+				break;
+
+			case AT_AlterColumnType:		/* alter column type */
+				appendStringInfo(&buf, " ALTER %s TYPE %s",
+								 cmd->name,
+								 TypeNameToString(def->typeName));
+				if (def->raw_default != NULL)
+				{
+					char *str =
+						deparse_expression_pretty(def->raw_default,
+												  NIL, false, false, 0, 0);
+					appendStringInfo(&buf, " USING %s", str);
+				}
+				break;
+
+			case AT_AlterColumnGenericOptions:	/* alter column OPTIONS (...) */
+				break;
+
+			case AT_ChangeOwner:			/* change owner */
+				appendStringInfo(&buf, " OWNER TO %s", cmd->name);
+				break;
+
+			case AT_ClusterOn:				/* CLUSTER ON */
+				appendStringInfo(&buf, " CLUSTER ON %s", cmd->name);
+				break;
+
+			case AT_DropCluster:			/* SET WITHOUT CLUSTER */
+				appendStringInfo(&buf, " SET WITHOUT CLUSTER");
+				break;
+
+			case AT_SetTableSpace:			/* SET TABLESPACE */
+				appendStringInfo(&buf, " SET TABLESPACE %s", cmd->name);
+				break;
+
+			case AT_SetRelOptions:			/* SET (...) -- AM specific parameters */
+				break;
+
+			case AT_ResetRelOptions:		/* RESET (...) -- AM specific parameters */
+				break;
+
+			case AT_EnableTrig:				/* ENABLE TRIGGER name */
+				appendStringInfo(&buf, " ENABLE TRIGGER %s", cmd->name);
+				break;
+
+			case AT_EnableAlwaysTrig:		/* ENABLE ALWAYS TRIGGER name */
+				appendStringInfo(&buf, " ENABLE ALWAYS TRIGGER %s", cmd->name);
+				break;
+
+			case AT_EnableReplicaTrig:		/* ENABLE REPLICA TRIGGER name */
+				appendStringInfo(&buf, " ENABLE REPLICA TRIGGER %s", cmd->name);
+				break;
+
+			case AT_DisableTrig:			/* DISABLE TRIGGER name */
+				appendStringInfo(&buf, " DISABLE TRIGGER %s", cmd->name);
+				break;
+
+			case AT_EnableTrigAll:			/* ENABLE TRIGGER ALL */
+				appendStringInfo(&buf, " ENABLE TRIGGER ALL");
+				break;
+
+			case AT_DisableTrigAll:			/* DISABLE TRIGGER ALL */
+				appendStringInfo(&buf, " DISABLE TRIGGER ALL");
+				break;
+
+			case AT_EnableTrigUser:			/* ENABLE TRIGGER USER */
+				appendStringInfo(&buf, " ENABLE TRIGGER USER");
+				break;
+
+			case AT_DisableTrigUser:		/* DISABLE TRIGGER USER */
+				appendStringInfo(&buf, " DISABLE TRIGGER USER");
+				break;
+
+			case AT_EnableRule:				/* ENABLE RULE name */
+				appendStringInfo(&buf, " ENABLE RULE %s", cmd->name);
+				break;
+
+			case AT_EnableAlwaysRule:		/* ENABLE ALWAYS RULE name */
+				appendStringInfo(&buf, " ENABLE ALWAYS RULE %s", cmd->name);
+				break;
+
+			case AT_EnableReplicaRule:		/* ENABLE REPLICA RULE name */
+				appendStringInfo(&buf, " ENABLE REPLICA RULE %s", cmd->name);
+				break;
+
+			case AT_DisableRule:			/* DISABLE RULE name */
+				appendStringInfo(&buf, " DISABLE RULE %s", cmd->name);
+				break;
+
+			case AT_AddInherit:				/* INHERIT parent */
+				appendStringInfo(&buf, " INHERIT %s",
+								 RangeVarToString((RangeVar *) cmd->def));
+				break;
+
+			case AT_DropInherit:			/* NO INHERIT parent */
+				appendStringInfo(&buf, " NO INHERIT %s",
+								 RangeVarToString((RangeVar *) cmd->def));
+				break;
+
+			case AT_AddOf:					/* OF <type_name> */
+				appendStringInfo(&buf, " OF %s", TypeNameToString(def->typeName));
+				break;
+
+			case AT_DropOf:					/* NOT OF */
+				appendStringInfo(&buf, " NOT OF");
+				break;
+
+			case AT_GenericOptions:			/* OPTIONS (...) */
+				break;
+
+			default:
+				break;
+		}
+	}
+	appendStringInfoChar(&buf, ';');
+
+	cmd->cmdstr = buf.data;
+	cmd->schemaname = node->relation->schemaname;
+	cmd->objectname = node->relation->relname;
+}
+
+/*
+ * get_cmddef
+ *
+ * internal use only, used for DDL Triggers
+ *
+ * Utility statements are not planned thus won't get into a Query *, we get to
+ * work from the parsetree directly, that would be query->utilityStmt which is
+ * of type Node *. We declare that a void * to avoid incompatible pointer type
+ * warnings.
+ */
+void
+pg_get_cmddef(CommandContext cmd, void *parsetree)
+{
+	/* cmd->nodestr = nodeToString(parsetree); */
+	cmd->nodestr = NULL;
+
+	/*
+	 * we need the big'o'switch here, and calling a specialized function per
+	 * utility statement nodetag.
+	 */
+
+	switch (nodeTag(parsetree))
+	{
+		case T_DropStmt:
+			_rwDropStmt(cmd, parsetree);
+			break;
+
+		case T_CreateStmt:
+			_rwCreateStmt(cmd, parsetree);
+			break;
+
+		case T_AlterTableStmt:
+			_rwAlterTableStmt(cmd, parsetree);
+			break;
+
+		case T_ViewStmt:
+			_rwViewStmt(cmd, parsetree);
+			break;
+
+		case T_CreateExtensionStmt:
+			_rwCreateExtensionStmt(cmd, parsetree);
+			break;
+
+		default:
+			/* is it best to elog(ERROR)?  Not while in development :) */
+			elog(DEBUG2, "unrecognized node type: %d",
+				 (int) nodeTag(parsetree));
+	}
+}
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 5302e50..a7220a0 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -146,6 +146,7 @@ typedef enum ObjectClass
 	OCLASS_USER_MAPPING,		/* pg_user_mapping */
 	OCLASS_DEFACL,				/* pg_default_acl */
 	OCLASS_EXTENSION,			/* pg_extension */
+	OCLASS_CMDTRIGGER,			/* pg_cmdtrigger */
 	MAX_OCLASS					/* MUST BE LAST */
 } ObjectClass;
 
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 0bf2d4d..7c1a81c 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -234,6 +234,11 @@ DECLARE_UNIQUE_INDEX(pg_trigger_tgrelid_tgname_index, 2701, on pg_trigger using
 DECLARE_UNIQUE_INDEX(pg_trigger_oid_index, 2702, on pg_trigger using btree(oid oid_ops));
 #define TriggerOidIndexId  2702
 
+DECLARE_UNIQUE_INDEX(pg_cmdtrigger_ctgcommand_ctgname_index, 3467, on pg_cmdtrigger using btree(ctgcommand name_ops, ctgname name_ops));
+#define CmdTriggerCommandNameIndexId  3467
+DECLARE_UNIQUE_INDEX(pg_cmdtrigger_oid_index, 3468, on pg_cmdtrigger using btree(oid oid_ops));
+#define CmdTriggerOidIndexId  3468
+
 DECLARE_UNIQUE_INDEX(pg_ts_config_cfgname_index, 3608, on pg_ts_config using btree(cfgname name_ops, cfgnamespace oid_ops));
 #define TSConfigNameNspIndexId	3608
 DECLARE_UNIQUE_INDEX(pg_ts_config_oid_index, 3712, on pg_ts_config using btree(oid oid_ops));
diff --git a/src/include/catalog/pg_cmdtrigger.h b/src/include/catalog/pg_cmdtrigger.h
new file mode 100644
index 0000000..dc12dad
--- /dev/null
+++ b/src/include/catalog/pg_cmdtrigger.h
@@ -0,0 +1,59 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_cmdtrigger.h
+ *	  definition of the system "command trigger" relation (pg_cmdtrigger)
+ *	  along with the relation's initial contents.
+ *
+ *
+ * Portions Copyright (c) 1996-2011, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_trigger.h
+ *
+ * NOTES
+ *	  the genbki.pl script reads this file and generates .bki
+ *	  information from the DATA() statements.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_CMDTRIGGER_H
+#define PG_CMDTRIGGER_H
+
+#include "catalog/genbki.h"
+
+/* ----------------
+ *		pg_cmdtrigger definition.	cpp turns this into
+ *		typedef struct FormData_pg_cmdtrigger
+ * ----------------
+ */
+#define CmdTriggerRelationId  3466
+
+CATALOG(pg_cmdtrigger,3466)
+{
+	NameData    ctgcommand;		/* trigger's command */
+	NameData	ctgname;		/* trigger's name */
+	Oid			ctgfoid;		/* OID of function to be called */
+	char		ctgtype;		/* BEFORE/AFTER/INSTEAD */
+	char		ctgenabled;		/* trigger's firing configuration WRT
+								 * session_replication_role */
+} FormData_pg_cmdtrigger;
+
+/* ----------------
+ *		Form_pg_cmdtrigger corresponds to a pointer to a tuple with
+ *		the format of pg_cmdtrigger relation.
+ * ----------------
+ */
+typedef FormData_pg_cmdtrigger *Form_pg_cmdtrigger;
+
+/* ----------------
+ *		compiler constants for pg_cmdtrigger
+ * ----------------
+ */
+#define Natts_pg_cmdtrigger					5
+#define Anum_pg_cmdtrigger_ctgcommand		1
+#define Anum_pg_cmdtrigger_ctgname			2
+#define Anum_pg_cmdtrigger_ctgfoid			3
+#define Anum_pg_cmdtrigger_ctgtype			4
+#define Anum_pg_cmdtrigger_ctgenabled		5
+
+#endif   /* PG_CMDTRIGGER_H */
diff --git a/src/include/commands/cmdtrigger.h b/src/include/commands/cmdtrigger.h
new file mode 100644
index 0000000..cc21d63
--- /dev/null
+++ b/src/include/commands/cmdtrigger.h
@@ -0,0 +1,43 @@
+/*-------------------------------------------------------------------------
+ *
+ * cmdtrigger.h
+ *	  Declarations for command trigger handling.
+ *
+ * Portions Copyright (c) 1996-2011, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/cmdtrigger.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef CMDTRIGGER_H
+#define CMDTRIGGER_H
+
+#include "nodes/execnodes.h"
+#include "nodes/parsenodes.h"
+
+/*
+ * Times at which a command trigger can be fired. These are the
+ * possible values for pg_cmdtrigger.ctgtype.
+ *
+ * pg_trigger is using binary mask tricks to make it super fast, but we don't
+ * need to be that tricky here: we're talking about commands, not data editing,
+ * and we don't have so many conditions, only type and enabled.
+ */
+#define CMD_TRIGGER_FIRED_BEFORE			'B'
+#define CMD_TRIGGER_FIRED_AFTER				'A'
+#define CMD_TRIGGER_FIRED_INSTEAD			'I'
+
+extern Oid CreateCmdTrigger(CreateCmdTrigStmt *stmt, const char *queryString);
+extern void DropCmdTrigger(DropCmdTrigStmt *stmt);
+extern void RemoveCmdTriggerById(Oid ctrigOid);
+extern Oid	get_cmdtrigger_oid(const char *trigname, const char *name, bool missing_ok);
+
+int ExecBeforeOrInsteadOfCommandTriggers(Node *parsetree, const char *command);
+int ExecInsteadOfCommandTriggers(Node *parsetree, const char *command,
+								 MemoryContext per_command_context);
+bool ExecBeforeCommandTriggers(Node *parsetree, const char *command,
+							   MemoryContext per_command_context);
+void ExecAfterCommandTriggers(Node *parsetree, const char *command);
+
+#endif   /* TRIGGER_H */
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index 64eeb73..f8db96a 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -185,4 +185,18 @@ extern TypeName *defGetTypeName(DefElem *def);
 extern int	defGetTypeLength(DefElem *def);
 extern DefElem *defWithOids(bool value);
 
+/* utils/adt/ruleutils.c -- FIXME, find a better place */
+typedef struct CommandContextData
+{
+	char *tag;					/* Command Tag */
+	char *cmdstr;				/* Command String, rewritten by ruleutils */
+	char *nodestr;				/* nodeToString(parsetree) */
+	char *schemaname;			/* schemaname or NULL if not relevant */
+	char *objectname;			/* objectname */
+} CommandContextData;
+
+typedef struct CommandContextData *CommandContext;
+
+extern void pg_get_cmddef(CommandContext cmd, void *parsetree);
+
 #endif   /* DEFREM_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 824d8b5..7764fa8 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -365,6 +365,8 @@ typedef enum NodeTag
 	T_CreateExtensionStmt,
 	T_AlterExtensionStmt,
 	T_AlterExtensionContentsStmt,
+	T_CreateCmdTrigStmt,
+	T_DropCmdTrigStmt,
 
 	/*
 	 * TAGS FOR PARSE TREE NODES (parsenodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index af6565e..dd59b27 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1106,6 +1106,7 @@ typedef enum ObjectType
 	OBJECT_AGGREGATE,
 	OBJECT_ATTRIBUTE,			/* type's attribute, when distinct from column */
 	OBJECT_CAST,
+	OBJECT_CMDTRIGGER,
 	OBJECT_COLUMN,
 	OBJECT_CONSTRAINT,
 	OBJECT_COLLATION,
@@ -1739,6 +1740,20 @@ typedef struct CreateTrigStmt
 } CreateTrigStmt;
 
 /* ----------------------
+ *		Create COMMAND TRIGGER Statement
+ * ----------------------
+ */
+typedef struct CreateCmdTrigStmt
+{
+	NodeTag		type;
+	char	   *command;		/* command's name */
+	char	   *trigname;			/* TRIGGER's name */
+	/* timing uses the TRIGGER_TYPE bits defined in catalog/pg_trigger.h */
+	int16		timing;			/* BEFORE, AFTER, or INSTEAD */
+	List	   *funcname;		/* qual. name of function to call */
+} CreateCmdTrigStmt;
+
+/* ----------------------
  *		Create/Drop PROCEDURAL LANGUAGE Statements
  * ----------------------
  */
@@ -1945,6 +1960,19 @@ typedef struct DropPropertyStmt
 } DropPropertyStmt;
 
 /* ----------------------
+ *		Drop Command Trigger Statement
+ * ----------------------
+ */
+typedef struct DropCmdTrigStmt
+{
+	NodeTag		type;
+	char	   *command;		/* command's name */
+	char	   *trigname;		/* TRIGGER's name */
+	DropBehavior behavior;		/* RESTRICT or CASCADE behavior */
+	bool		missing_ok;		/* skip error if missing? */
+} DropCmdTrigStmt;
+
+/* ----------------------
  *				Truncate Table Statement
  * ----------------------
  */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 3d170bc..bb949d0 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -81,6 +81,7 @@ PG_KEYWORD("coalesce", COALESCE, COL_NAME_KEYWORD)
 PG_KEYWORD("collate", COLLATE, RESERVED_KEYWORD)
 PG_KEYWORD("collation", COLLATION, UNRESERVED_KEYWORD)
 PG_KEYWORD("column", COLUMN, RESERVED_KEYWORD)
+PG_KEYWORD("command", COMMAND, UNRESERVED_KEYWORD)
 PG_KEYWORD("comment", COMMENT, UNRESERVED_KEYWORD)
 PG_KEYWORD("comments", COMMENTS, UNRESERVED_KEYWORD)
 PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD)
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 38c88d9..c5f1392 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -92,6 +92,7 @@ SELECT relname, relhasindex
  pg_authid               | t
  pg_cast                 | t
  pg_class                | t
+ pg_cmdtrigger           | t
  pg_collation            | t
  pg_constraint           | t
  pg_conversion           | t
@@ -159,7 +160,7 @@ SELECT relname, relhasindex
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(148 rows)
+(149 rows)
 
 --
 -- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index b4d3919..3892d36 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -1443,3 +1443,38 @@ NOTICE:  drop cascades to 2 other objects
 DETAIL:  drop cascades to view city_view
 drop cascades to view european_city_view
 DROP TABLE country_table;
+CREATE FUNCTION cmdtrigger_notice
+ (
+   IN cmd_string     text,
+   IN cmd_nodestring text,
+   IN schemaname     text,
+   IN relname        text
+ )
+ RETURNS void
+ LANGUAGE plpgsql
+AS $$
+BEGIN
+  RAISE NOTICE 'cmd_string: %', cmd_string;
+END;
+$$;
+CREATE TRIGGER cmdtrigger_notice
+        AFTER COMMAND CREATE TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+CREATE TRIGGER cmdtrigger_notice
+        AFTER COMMAND DROP TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+-- that should error out as you can't have both INSTEAD OF command triggers
+-- and BEFORE|AFTER triggers defined on the same command
+CREATE TRIGGER cmdtrigger_notice_error
+    INSTEAD OF COMMAND DROP TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+ERROR:  "DROP TABLE" already has AFTER triggers
+DETAIL:  Commands cannot have both AFTER and INSTEAD OF triggers.
+CREATE TABLE foo(a serial, b text, primary key (a, b));
+NOTICE:  CREATE TABLE will create implicit sequence "foo_a_seq" for serial column "foo.a"
+NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"
+NOTICE:  cmd_string: CREATE TABLE foo (a serial,b text, PRIMARY KEY(a,b));
+DROP TABLE foo;
+NOTICE:  cmd_string: DROP TABLE foo RESTRICT;
+DROP TRIGGER cmdtrigger_notice ON COMMAND CREATE TABLE;
+DROP TRIGGER cmdtrigger_notice ON COMMAND DROP TABLE;
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index e52131d..f944d60 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -961,3 +961,38 @@ SELECT * FROM city_view;
 
 DROP TABLE city_table CASCADE;
 DROP TABLE country_table;
+
+CREATE FUNCTION cmdtrigger_notice
+ (
+   IN cmd_string     text,
+   IN cmd_nodestring text,
+   IN schemaname     text,
+   IN relname        text
+ )
+ RETURNS void
+ LANGUAGE plpgsql
+AS $$
+BEGIN
+  RAISE NOTICE 'cmd_string: %', cmd_string;
+END;
+$$;
+
+CREATE TRIGGER cmdtrigger_notice
+        AFTER COMMAND CREATE TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+
+CREATE TRIGGER cmdtrigger_notice
+        AFTER COMMAND DROP TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+
+-- that should error out as you can't have both INSTEAD OF command triggers
+-- and BEFORE|AFTER triggers defined on the same command
+CREATE TRIGGER cmdtrigger_notice_error
+    INSTEAD OF COMMAND DROP TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+
+CREATE TABLE foo(a serial, b text, primary key (a, b));
+DROP TABLE foo;
+
+DROP TRIGGER cmdtrigger_notice ON COMMAND CREATE TABLE;
+DROP TRIGGER cmdtrigger_notice ON COMMAND DROP TABLE;
#2Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Dimitri Fontaine (#1)
1 attachment(s)
Re: Command Triggers

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

As proposed by Jan Wieck on hackers and discussed in Ottawa at the
Clusters Hackers Meeting, the most (only?) workable way to ever have DDL
triggers is to have "command triggers" instead.

[...]

The v1 patch attached contains implementation for some commands only.

Here's a v2 which is only about cleaning up the merge from master. The
recent DROP rework conflicted badly with the command triggers patch,
quite obviously. I didn't reimplement DROP COMMAND TRIGGER in terms of
the new facilities yet, though.

So, any comment on the approach before I complete the rewriting of the
commands, the out/read funcs, and add more commands to the trigger
support code?

https://github.com/dimitri/postgres/compare/master...command_triggers

FWIW (scheduling mainly), I don't think I'm going to spend more time on
this work until I get some comments, because all that remains to be done
is about building on top of what I've already been doing here.

Well, I'd like to implement command triggers on replica too, but baring
psql support and a bunch of commands not there yet, that's about it as
far as features go.

Oh, and have expressions rewriting from the parsetree (default, check)
actually work (rather than segfault) is still a TODO too.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

Attachments:

command-trigger.v2.patchtext/x-patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index a7915cf..059e6bb 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -29,12 +29,26 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
     [ WHEN ( <replaceable class="parameter">condition</replaceable> ) ]
     EXECUTE PROCEDURE <replaceable class="PARAMETER">function_name</replaceable> ( <replaceable class="PARAMETER">arguments</replaceable> )
 
+CREATE TRIGGER <replaceable class="PARAMETER">name</replaceable> { BEFORE | AFTER | INSTEAD OF } COMMAND <replaceable class="PARAMETER">command</replaceable>
+    EXECUTE PROCEDURE <replaceable class="PARAMETER">function_name</replaceable> ( <replaceable class="PARAMETER">arguments</replaceable> )
+
 <phrase>where <replaceable class="parameter">event</replaceable> can be one of:</phrase>
 
     INSERT
     UPDATE [ OF <replaceable class="parameter">column_name</replaceable> [, ... ] ]
     DELETE
     TRUNCATE
+
+<phrase>and where <replaceable class="parameter">command</replaceable> can be one of:</phrase>
+
+    CREATE TABLE
+    ALTER TABLE
+    DROP TABLE
+    CREATE VIEW
+    DROP VIEW
+    CREATE EXTENSION
+    DROP EXTENSION
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,10 +56,20 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="PARAMETER">name</replaceable>
   <title>Description</title>
 
   <para>
-   <command>CREATE TRIGGER</command> creates a new trigger.  The
-   trigger will be associated with the specified table or view and will
-   execute the specified function <replaceable
-   class="parameter">function_name</replaceable> when certain events occur.
+   <command>CREATE TRIGGER</command> creates a new trigger. The trigger will
+   be associated with the specified table, view or command and will execute
+   the specified
+   function <replaceable class="parameter">function_name</replaceable> when
+   certain events occur.
+  </para>
+
+  <para>
+   The command trigger can be specified to fire before or after the command
+   is executed, or instead of executing the command. A before trigger's
+   function must return a boolean, returning <literal>False</literal> allows
+   the procedure to prevent the command execution. AFTER and INSTEAD OF
+   triggers return value is not used, such trigger's function must then
+   return <literal>void</literal>.
   </para>
 
   <para>
@@ -251,6 +275,15 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
    </varlistentry>
 
    <varlistentry>
+    <term><replaceable class="parameter">command</replaceable></term>
+    <listitem>
+     <para>
+      The tag of the command the trigger is for.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><replaceable class="PARAMETER">referenced_table_name</replaceable></term>
     <listitem>
      <para>
@@ -334,7 +367,23 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
      <para>
       A user-supplied function that is declared as taking no arguments
       and returning type <literal>trigger</>, which is executed when
-      the trigger fires.
+      the trigger fires, for table and view triggers.
+     </para>
+     <para>
+      In the case of a BEFORE COMMAND trigger, the user-supplied function
+      must be declared as taking 4 text arguments and returning a boolean.
+      In the case of an AFTER COMMAND trigger or an INSTEAD OF trigger, the
+      user-supplied function must be declare as taking 4 text arguments and
+      returning void.
+     </para>
+     <para>
+      The command trigger function is called with the
+      parameters <literal>command string</literal> (normalized command
+      string rewritten by PostgreSQL), <literal>command node
+      string</literal> (the command syntax tree, in its text form, is easier
+      to deal with programmaticaly for more complex inquiries about the
+      command), <literal>schemaname</literal> (can be null)
+      and <literal>object name</literal>.
      </para>
     </listitem>
    </varlistentry>
@@ -352,6 +401,10 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
       can be accessed within the function; it might be different from
       normal function arguments.
      </para>
+     <para>
+      Command triggers pay no attention to
+      the <replaceable class="parameter">arguments</replaceable>.
+     </para>
     </listitem>
    </varlistentry>
   </variablelist>
@@ -469,6 +522,35 @@ CREATE TRIGGER view_insert
     FOR EACH ROW
     EXECUTE PROCEDURE view_insert_row();
 </programlisting>
+
+   Execute the function <function>enforce_local_style</> each time
+   a <literal>CREATE TABLE</literal> command is run:
+
+<programlisting>
+CREATE OR REPLACE FUNCTION enforce_local_style
+ (
+   IN cmd_string     text,
+   IN cmd_nodestring text,
+   IN schemaname     text,
+   IN relname        text
+ )
+ RETURNS bool
+ LANGUAGE plpgsql
+AS $$
+BEGIN
+  IF substring(relname, 0, 4) NOT IN ('ab_', 'cz_', 'fr_')
+  THEN
+    RAISE WARNING 'invalid relation name: %', relname;
+    RETURN FALSE;
+  END IF;
+  RETURN TRUE;
+END;
+$$;
+
+CREATE TRIGGER check_style
+        BEFORE COMMAND CREATE TABLE
+       EXECUTE PROCEDURE enforce_local_style();
+</programlisting>
   </para>
 
   <para>
@@ -531,6 +613,11 @@ CREATE TRIGGER view_insert
   </para>
 
   <para>
+   The ability to run triggers on commands is <productname>PostgreSQL</>
+   extension of the SQL standard.
+  </para>
+
+  <para>
    The ability to specify multiple actions for a single trigger using
    <literal>OR</literal> is a <productname>PostgreSQL</> extension of
    the SQL standard.
diff --git a/doc/src/sgml/ref/drop_trigger.sgml b/doc/src/sgml/ref/drop_trigger.sgml
index 2fcc690..3e37aa2 100644
--- a/doc/src/sgml/ref/drop_trigger.sgml
+++ b/doc/src/sgml/ref/drop_trigger.sgml
@@ -22,6 +22,18 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 DROP TRIGGER [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable> ON <replaceable class="PARAMETER">table</replaceable> [ CASCADE | RESTRICT ]
+DROP TRIGGER [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable> ON COMMAND <replaceable class="PARAMETER">command</replaceable> [ CASCADE | RESTRICT ]
+
+<phrase>where <replaceable class="parameter">command</replaceable> can be one of:</phrase>
+
+    CREATE TABLE
+    ALTER TABLE
+    DROP TABLE
+    CREATE VIEW
+    DROP VIEW
+    CREATE EXTENSION
+    DROP EXTENSION
+
 </synopsis>
  </refsynopsisdiv>
 
@@ -29,9 +41,10 @@ DROP TRIGGER [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable> ON
   <title>Description</title>
 
   <para>
-   <command>DROP TRIGGER</command> removes an existing
-   trigger definition. To execute this command, the current
-   user must be the owner of the table for which the trigger is defined.
+   <command>DROP TRIGGER</command> removes an existing trigger definition.
+   To execute this command, the current user must be the owner of the table
+   for which the trigger is defined, or a database owner in case of a
+   command trigger.
   </para>
  </refsect1>
 
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 5a4419d..7e7d4c6 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -31,7 +31,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_attrdef.h pg_constraint.h pg_inherits.h pg_index.h pg_operator.h \
 	pg_opfamily.h pg_opclass.h pg_am.h pg_amop.h pg_amproc.h \
 	pg_language.h pg_largeobject_metadata.h pg_largeobject.h pg_aggregate.h \
-	pg_statistic.h pg_rewrite.h pg_trigger.h pg_description.h \
+	pg_statistic.h pg_rewrite.h pg_trigger.h pg_cmdtrigger.h pg_description.h \
 	pg_cast.h pg_enum.h pg_namespace.h pg_conversion.h pg_depend.h \
 	pg_database.h pg_db_role_setting.h pg_tablespace.h pg_pltemplate.h \
 	pg_authid.h pg_auth_members.h pg_shdepend.h pg_shdescription.h \
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 223c097..42701f3 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -25,6 +25,7 @@
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_cmdtrigger.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_collation_fn.h"
 #include "catalog/pg_constraint.h"
@@ -52,6 +53,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "commands/cmdtrigger.h"
 #include "commands/comment.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
@@ -157,7 +159,8 @@ static const Oid object_classes[MAX_OCLASS] = {
 	ForeignServerRelationId,	/* OCLASS_FOREIGN_SERVER */
 	UserMappingRelationId,		/* OCLASS_USER_MAPPING */
 	DefaultAclRelationId,		/* OCLASS_DEFACL */
-	ExtensionRelationId			/* OCLASS_EXTENSION */
+	ExtensionRelationId,		/* OCLASS_EXTENSION */
+	CmdTriggerRelationId		/* OCLASS_CMDTRIGGER */
 };
 
 
@@ -1052,6 +1055,10 @@ doDeletion(const ObjectAddress *object)
 				break;
 			}
 
+		case OCLASS_CMDTRIGGER:
+			RemoveCmdTriggerById(object->objectId);
+			break;
+
 		case OCLASS_PROC:
 			RemoveFunctionById(object->objectId);
 			break;
@@ -2173,6 +2180,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case ExtensionRelationId:
 			return OCLASS_EXTENSION;
+
+		case CmdTriggerRelationId:
+			return OCLASS_CMDTRIGGER;
 	}
 
 	/* shouldn't get here */
@@ -2807,6 +2817,41 @@ getObjectDescription(const ObjectAddress *object)
 				break;
 			}
 
+        case OCLASS_CMDTRIGGER:
+			{
+				Relation	trigDesc;
+				ScanKeyData skey[1];
+				SysScanDesc tgscan;
+				HeapTuple	tup;
+				Form_pg_cmdtrigger trig;
+
+				trigDesc = heap_open(CmdTriggerRelationId, AccessShareLock);
+
+				ScanKeyInit(&skey[0],
+							ObjectIdAttributeNumber,
+							BTEqualStrategyNumber, F_OIDEQ,
+							ObjectIdGetDatum(object->objectId));
+
+				tgscan = systable_beginscan(trigDesc, CmdTriggerOidIndexId, true,
+											SnapshotNow, 1, skey);
+
+				tup = systable_getnext(tgscan);
+
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "could not find tuple for command trigger %u",
+						 object->objectId);
+
+				trig = (Form_pg_cmdtrigger) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("trigger %s on %s"),
+								 NameStr(trig->ctgname),
+								 NameStr(trig->ctgcommand));
+
+				systable_endscan(tgscan);
+				heap_close(trigDesc, AccessShareLock);
+				break;
+			}
+
 		default:
 			appendStringInfo(&buffer, "unrecognized object %u %u %d",
 							 object->classId,
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 30873aa..67f6fab 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -21,6 +21,7 @@
 #include "catalog/objectaddress.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_cmdtrigger.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
@@ -44,6 +45,7 @@
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
+#include "commands/cmdtrigger.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/extension.h"
@@ -204,6 +206,12 @@ static ObjectPropertyType ObjectProperty[] =
 		InvalidAttrNumber
 	},
 	{
+		CmdTriggerRelationId,
+		CmdTriggerOidIndexId,
+		-1,
+		InvalidAttrNumber
+	},
+	{
 		TSConfigRelationId,
 		TSConfigOidIndexId,
 		TSCONFIGOID,
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 4af7aad..a00a358 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -12,8 +12,8 @@ subdir = src/backend/commands
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
-OBJS = aggregatecmds.o alter.o analyze.o async.o cluster.o comment.o  \
-	collationcmds.o constraint.o conversioncmds.o copy.o \
+OBJS = aggregatecmds.o alter.o analyze.o async.o cluster.o cmdtrigger.o \
+	comment.o collationcmds.o constraint.o conversioncmds.o copy.o \
 	dbcommands.o define.o discard.o dropcmds.o explain.o extension.o \
 	foreigncmds.o functioncmds.o \
 	indexcmds.o lockcmds.o operatorcmds.o opclasscmds.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index c321224..505d672 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -61,6 +61,10 @@ ExecRenameStmt(RenameStmt *stmt)
 			RenameConversion(stmt->object, stmt->newname);
 			break;
 
+		case OBJECT_CMDTRIGGER:
+			RenameCmdTrigger(stmt->object, stmt->subname, stmt->newname);
+			break;
+
 		case OBJECT_DATABASE:
 			RenameDatabase(stmt->subname, stmt->newname);
 			break;
diff --git a/src/backend/commands/cmdtrigger.c b/src/backend/commands/cmdtrigger.c
new file mode 100644
index 0000000..5a4d370
--- /dev/null
+++ b/src/backend/commands/cmdtrigger.c
@@ -0,0 +1,708 @@
+/*-------------------------------------------------------------------------
+ *
+ * trigger.c
+ *	  PostgreSQL TRIGGERs support code.
+ *
+ * Portions Copyright (c) 1996-2011, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/trigger.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/sysattr.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_cmdtrigger.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_trigger.h"
+#include "catalog/pg_type.h"
+#include "commands/defrem.h"
+#include "commands/cmdtrigger.h"
+#include "commands/trigger.h"
+#include "parser/parse_func.h"
+#include "pgstat.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/tqual.h"
+
+static void check_cmdtrigger_name(const char *command, const char *trigname, Relation tgrel);
+static RegProcedure * list_triggers_for_command(const char *command, char type);
+
+/*
+ * Create a trigger.  Returns the OID of the created trigger.
+ */
+Oid
+CreateCmdTrigger(CreateCmdTrigStmt *stmt, const char *queryString)
+{
+	Relation	tgrel;
+	HeapTuple	tuple;
+	Datum		values[Natts_pg_trigger];
+	bool		nulls[Natts_pg_trigger];
+	/* cmd trigger args: cmd_string, cmd_nodestring, schemaname, objectname */
+	Oid			fargtypes[4] = {TEXTOID, TEXTOID, TEXTOID, TEXTOID};
+	Oid			funcoid;
+	Oid			funcrettype;
+	Oid			trigoid;
+	char        ctgtype;
+	ObjectAddress myself,
+				referenced;
+
+	/*
+	 * Find and validate the trigger function.
+	 */
+	funcoid = LookupFuncName(stmt->funcname, 4, fargtypes, false);
+	funcrettype = get_func_rettype(funcoid);
+
+	/*
+	 * Generate the trigger's OID now, so that we can use it in the name if
+	 * needed.
+	 */
+	tgrel = heap_open(CmdTriggerRelationId, RowExclusiveLock);
+
+	/*
+	 * Scan pg_cmdtrigger for existing triggers on command. We do this only to
+	 * give a nice error message if there's already a trigger of the same name.
+	 * (The unique index on ctgcommand/ctgname would complain anyway.)
+	 *
+	 * NOTE that this is cool only because we have AccessExclusiveLock on
+	 * the relation, so the trigger set won't be changing underneath us.
+	 */
+	check_cmdtrigger_name(stmt->command, stmt->trigname, tgrel);
+
+	switch (stmt->timing)
+	{
+		case TRIGGER_TYPE_BEFORE:
+		{
+	        RegProcedure *procs = list_triggers_for_command(stmt->command, CMD_TRIGGER_FIRED_INSTEAD);
+		    ctgtype = CMD_TRIGGER_FIRED_BEFORE;
+			if (procs[0] != InvalidOid)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("\"%s\" already has INSTEAD OF triggers",
+								stmt->command),
+						 errdetail("Commands cannot have both BEFORE and INSTEAD OF triggers.")));
+            break;
+		}
+
+		case TRIGGER_TYPE_INSTEAD:
+		{
+	        RegProcedure *before = list_triggers_for_command(stmt->command, CMD_TRIGGER_FIRED_BEFORE);
+	        RegProcedure *after;
+		    ctgtype = CMD_TRIGGER_FIRED_INSTEAD;
+			if (before[0] != InvalidOid)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("\"%s\" already has BEFORE triggers",
+								stmt->command),
+						 errdetail("Commands cannot have both BEFORE and INSTEAD OF triggers.")));
+
+			after = list_triggers_for_command(stmt->command, CMD_TRIGGER_FIRED_AFTER);
+			if (after[0] != InvalidOid)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("\"%s\" already has AFTER triggers",
+								stmt->command),
+						 errdetail("Commands cannot have both AFTER and INSTEAD OF triggers.")));
+            break;
+		}
+
+		case TRIGGER_TYPE_AFTER:
+		{
+	        RegProcedure *procs = list_triggers_for_command(stmt->command, CMD_TRIGGER_FIRED_INSTEAD);
+		    ctgtype = CMD_TRIGGER_FIRED_AFTER;
+			if (procs[0] != InvalidOid)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("\"%s\" already has INSTEAD OF triggers",
+								stmt->command),
+						 errdetail("Commands cannot have both AFTER and INSTEAD OF triggers.")));
+            break;
+		}
+	}
+
+	if (ctgtype == CMD_TRIGGER_FIRED_BEFORE && funcrettype != BOOLOID)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("function \"%s\" must return type \"boolean\"",
+						NameListToString(stmt->funcname))));
+
+	if (ctgtype != CMD_TRIGGER_FIRED_BEFORE && funcrettype != VOIDOID)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("function \"%s\" must return type \"void\"",
+						NameListToString(stmt->funcname))));
+
+	/*
+	 * Build the new pg_trigger tuple.
+	 */
+	memset(nulls, false, sizeof(nulls));
+
+	values[Anum_pg_cmdtrigger_ctgcommand - 1] = NameGetDatum(stmt->command);
+	values[Anum_pg_cmdtrigger_ctgname - 1] = NameGetDatum(stmt->trigname);
+	values[Anum_pg_cmdtrigger_ctgfoid - 1] = ObjectIdGetDatum(funcoid);
+	values[Anum_pg_cmdtrigger_ctgtype - 1] = CharGetDatum(ctgtype);
+	values[Anum_pg_cmdtrigger_ctgenabled - 1] = CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
+
+	tuple = heap_form_tuple(tgrel->rd_att, values, nulls);
+
+	/* force tuple to have the desired OID */
+	trigoid = HeapTupleGetOid(tuple);
+
+	/*
+	 * Insert tuple into pg_trigger.
+	 */
+	simple_heap_insert(tgrel, tuple);
+
+	CatalogUpdateIndexes(tgrel, tuple);
+
+	heap_freetuple(tuple);
+	heap_close(tgrel, RowExclusiveLock);
+
+	/*
+	 * Record dependencies for trigger.  Always place a normal dependency on
+	 * the function.
+	 */
+	myself.classId = TriggerRelationId;
+	myself.objectId = trigoid;
+	myself.objectSubId = 0;
+
+	referenced.classId = ProcedureRelationId;
+	referenced.objectId = funcoid;
+	referenced.objectSubId = 0;
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	return trigoid;
+}
+
+/*
+ * DropTrigger - drop an individual trigger by name
+ */
+void
+DropCmdTrigger(DropCmdTrigStmt *stmt)
+{
+	ObjectAddress object;
+
+	object.classId = CmdTriggerRelationId;
+	object.objectId = get_cmdtrigger_oid(stmt->command, stmt->trigname,
+										 stmt->missing_ok);
+	object.objectSubId = 0;
+
+	if (!OidIsValid(object.objectId))
+	{
+		ereport(NOTICE,
+		  (errmsg("trigger \"%s\" for command \"%s\" does not exist, skipping",
+				  stmt->trigname, stmt->command)));
+		return;
+	}
+
+	/*
+	 * Do the deletion
+	 */
+	performDeletion(&object, stmt->behavior);
+}
+
+/*
+ * Guts of command trigger deletion.
+ */
+void
+RemoveCmdTriggerById(Oid trigOid)
+{
+	Relation	tgrel;
+	SysScanDesc tgscan;
+	ScanKeyData skey[1];
+	HeapTuple	tup;
+
+	tgrel = heap_open(CmdTriggerRelationId, RowExclusiveLock);
+
+	/*
+	 * Find the trigger to delete.
+	 */
+	ScanKeyInit(&skey[0],
+				ObjectIdAttributeNumber,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(trigOid));
+
+	tgscan = systable_beginscan(tgrel, CmdTriggerOidIndexId, true,
+								SnapshotNow, 1, skey);
+
+	tup = systable_getnext(tgscan);
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "could not find tuple for command trigger %u", trigOid);
+
+	/*
+	 * Delete the pg_cmdtrigger tuple.
+	 */
+	simple_heap_delete(tgrel, &tup->t_self);
+
+	systable_endscan(tgscan);
+	heap_close(tgrel, RowExclusiveLock);
+}
+
+/*
+ * ALTER TRIGGER foo ON COMMAND ... ENABLE|DISABLE|ENABLE ALWAYS|REPLICA
+ */
+void
+AlterCmdTrigger(AlterCmdTrigStmt *stmt)
+{
+	Relation	tgrel;
+	SysScanDesc tgscan;
+	ScanKeyData skey[2];
+	HeapTuple	tup;
+	Form_pg_cmdtrigger cmdForm;
+	char        tgenabled = pstrdup(stmt->tgenabled)[0]; /* works with gram.y */
+
+	tgrel = heap_open(CmdTriggerRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_cmdtrigger_ctgcommand,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(stmt->command));
+	ScanKeyInit(&skey[1],
+				Anum_pg_cmdtrigger_ctgname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(stmt->trigname));
+
+	tgscan = systable_beginscan(tgrel, CmdTriggerCommandNameIndexId, true,
+								SnapshotNow, 2, skey);
+
+	tup = systable_getnext(tgscan);
+
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("trigger \"%s\" for command \"%s\" does not exist, skipping",
+						stmt->trigname, stmt->command)));
+
+	/* Copy tuple so we can modify it below */
+	tup = heap_copytuple(tup);
+	cmdForm = (Form_pg_cmdtrigger) GETSTRUCT(tup);
+
+	systable_endscan(tgscan);
+
+	cmdForm->ctgenabled = tgenabled;
+
+	simple_heap_update(tgrel, &tup->t_self, tup);
+	CatalogUpdateIndexes(tgrel, tup);
+
+	heap_close(tgrel, AccessShareLock);
+	heap_freetuple(tup);
+}
+
+
+/*
+ * Rename conversion
+ */
+void
+RenameCmdTrigger(List *name, const char *trigname, const char *newname)
+{
+	SysScanDesc tgscan;
+	ScanKeyData skey[2];
+	HeapTuple	tup;
+	Relation	rel;
+	Form_pg_cmdtrigger cmdForm;
+	char       *command;
+
+	Assert(list_length(name) == 1);
+	command = strVal((Value *)linitial(name));
+
+	rel = heap_open(CmdTriggerRelationId, RowExclusiveLock);
+
+	/* newname must be available */
+	check_cmdtrigger_name(command, newname, rel);
+
+	/* get existing tuple */
+	ScanKeyInit(&skey[0],
+				Anum_pg_cmdtrigger_ctgcommand,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(command));
+	ScanKeyInit(&skey[1],
+				Anum_pg_cmdtrigger_ctgname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(trigname));
+
+	tgscan = systable_beginscan(rel, CmdTriggerCommandNameIndexId, true,
+								SnapshotNow, 2, skey);
+
+	tup = systable_getnext(tgscan);
+
+	if (!HeapTupleIsValid(tup))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("trigger \"%s\" for command \"%s\" does not exist, skipping",
+						trigname, command)));
+
+	/* Copy tuple so we can modify it below */
+	tup = heap_copytuple(tup);
+	cmdForm = (Form_pg_cmdtrigger) GETSTRUCT(tup);
+
+	systable_endscan(tgscan);
+
+	/* rename */
+	namestrcpy(&(cmdForm->ctgname), newname);
+	simple_heap_update(rel, &tup->t_self, tup);
+	CatalogUpdateIndexes(rel, tup);
+
+	heap_freetuple(tup);
+	heap_close(rel, NoLock);
+}
+
+/*
+ * get_cmdtrigger_oid - Look up a trigger by name to find its OID.
+ *
+ * If missing_ok is false, throw an error if trigger not found.  If
+ * true, just return InvalidOid.
+ */
+Oid
+get_cmdtrigger_oid(const char *command, const char *trigname, bool missing_ok)
+{
+	Relation	tgrel;
+	ScanKeyData skey[2];
+	SysScanDesc tgscan;
+	HeapTuple	tup;
+	Oid			oid;
+
+	/*
+	 * Find the trigger, verify permissions, set up object address
+	 */
+	tgrel = heap_open(CmdTriggerRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_cmdtrigger_ctgcommand,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(command));
+	ScanKeyInit(&skey[1],
+				Anum_pg_cmdtrigger_ctgname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(trigname));
+
+	tgscan = systable_beginscan(tgrel, CmdTriggerCommandNameIndexId, true,
+								SnapshotNow, 1, skey);
+
+	tup = systable_getnext(tgscan);
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("trigger \"%s\" for command \"%s\" does not exist, skipping",
+							 trigname, command)));
+		oid = InvalidOid;
+	}
+	else
+	{
+		oid = HeapTupleGetOid(tup);
+	}
+
+	systable_endscan(tgscan);
+	heap_close(tgrel, AccessShareLock);
+	return oid;
+}
+
+/*
+ * Scan pg_cmdtrigger for existing triggers on command. We do this only to
+ * give a nice error message if there's already a trigger of the same name.
+ */
+void
+check_cmdtrigger_name(const char *command, const char *trigname, Relation tgrel)
+{
+	SysScanDesc tgscan;
+	ScanKeyData skey[2];
+	HeapTuple	tuple;
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_cmdtrigger_ctgcommand,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(command));
+	ScanKeyInit(&skey[1],
+				Anum_pg_cmdtrigger_ctgname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(trigname));
+
+	tgscan = systable_beginscan(tgrel, CmdTriggerCommandNameIndexId, true,
+								SnapshotNow, 2, skey);
+
+	tuple = systable_getnext(tgscan);
+
+	elog(NOTICE, "check_cmdtrigger_name(%s, %s)", command, trigname);
+
+	if (HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+				 errmsg("trigger \"%s\" for command \"%s\" already exists",
+						trigname, command)));
+	systable_endscan(tgscan);
+}
+
+/*
+ * Functions to execute the command triggers.
+ *
+ * We call the functions that matches the command triggers definitions in
+ * alphabetical order, and give them those arguments:
+ *
+ *   command string, text
+ *   command node string, text
+ *   schemaname, text, can be null
+ *   objectname, text
+ *
+ * we rebuild the DDL command we're about to execute from the parsetree.
+ *
+ * The queryString comes from untrusted places: it could be a multiple
+ * queries string that has been passed through psql -c or otherwise in the
+ * protocol, or something that comes from an EXECUTE evaluation in plpgsql.
+ *
+ * Also we need to be able to spit out a normalized (canonical?) SQL
+ * command to ease DDL trigger code, and we even provide them with a
+ * nodeToString() output.
+ *
+ */
+
+static RegProcedure *
+list_triggers_for_command(const char *command, char type)
+{
+	int  count = 0, size = 10;
+	RegProcedure *procs = (RegProcedure *) palloc(size*sizeof(RegProcedure));
+
+	Relation	rel, irel;
+	SysScanDesc scandesc;
+	HeapTuple	tuple;
+	ScanKeyData entry[1];
+
+	/* init the first entry of the procs array */
+	procs[0] = InvalidOid;
+
+	rel = heap_open(CmdTriggerRelationId, AccessShareLock);
+	irel = index_open(CmdTriggerCommandNameIndexId, AccessShareLock);
+
+	ScanKeyInit(&entry[0],
+				Anum_pg_cmdtrigger_ctgcommand,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(command));
+
+	scandesc = systable_beginscan_ordered(rel, irel, SnapshotNow, 1, entry);
+
+	while (HeapTupleIsValid(tuple = systable_getnext_ordered(scandesc, ForwardScanDirection)))
+	{
+		Form_pg_cmdtrigger cmd = (Form_pg_cmdtrigger) GETSTRUCT(tuple);
+
+        /*
+		 * Replica support for command triggers is still on the TODO
+		 */
+		if (cmd->ctgenabled != 'D' && cmd->ctgtype == type)
+		{
+			if (count == size)
+			{
+				size += 10;
+				procs = (Oid *)repalloc(procs, size);
+			}
+			procs[count++] = cmd->ctgfoid;
+			procs[count] = InvalidOid;
+		}
+	}
+	systable_endscan_ordered(scandesc);
+
+	index_close(irel, AccessShareLock);
+	heap_close(rel, AccessShareLock);
+
+	return procs;
+}
+
+static bool
+call_cmdtrigger_procedure(RegProcedure proc, CommandContext cmd,
+						  MemoryContext per_command_context)
+{
+	FmgrInfo	flinfo;
+	FunctionCallInfoData fcinfo;
+	PgStat_FunctionCallUsage fcusage;
+	Datum		result;
+
+	fmgr_info_cxt(proc, &flinfo, per_command_context);
+
+	/* Can't use OidFunctionCallN because we might get a NULL result */
+	InitFunctionCallInfoData(fcinfo, &flinfo, 4, InvalidOid, NULL, NULL);
+
+	fcinfo.arg[0] = PointerGetDatum(cstring_to_text(pstrdup(cmd->cmdstr)));
+
+	if (cmd->nodestr != NULL)
+		fcinfo.arg[1] = PointerGetDatum(cstring_to_text(pstrdup(cmd->nodestr)));
+
+	if (cmd->schemaname != NULL)
+		fcinfo.arg[2] = PointerGetDatum(cstring_to_text(pstrdup(cmd->schemaname)));
+
+	fcinfo.arg[3] = PointerGetDatum(cstring_to_text(pstrdup(cmd->objectname)));
+
+	fcinfo.argnull[0] = false;
+	fcinfo.argnull[1] = cmd->nodestr == NULL;
+	fcinfo.argnull[2] = cmd->schemaname == NULL;
+	fcinfo.argnull[3] = false;
+
+	pgstat_init_function_usage(&fcinfo, &fcusage);
+
+	result = FunctionCallInvoke(&fcinfo);
+
+	pgstat_end_function_usage(&fcusage, true);
+
+	if (!fcinfo.isnull && DatumGetBool(result) == false)
+		return false;
+	return true;
+}
+
+/*
+ * For any given command tag, you can have either Before and After triggers, or
+ * Instead Of triggers, not both.
+ *
+ * Instead Of triggers have to run before the command and to cancel its
+ * execution , hence this API where we return the number of InsteadOf trigger
+ * procedures we fired.
+ */
+int
+ExecBeforeOrInsteadOfCommandTriggers(Node *parsetree, const char *cmdtag)
+{
+	MemoryContext per_command_context;
+	int nb = 0;
+
+	per_command_context =
+		AllocSetContextCreate(CurrentMemoryContext,
+							  "BeforeOrInsteadOfTriggerCommandContext",
+							  ALLOCSET_DEFAULT_MINSIZE,
+							  ALLOCSET_DEFAULT_INITSIZE,
+							  ALLOCSET_DEFAULT_MAXSIZE);
+
+	/*
+	 * You can't have both BEFORE and INSTEAD OF triggers registered on the
+	 * same command, so this function is not checking about that and just going
+	 * through an empty list in at least one of those cases.  The cost of doing
+	 * it this lazy way is an index scan on pg_catalog.pg_cmdtrigger.
+	 */
+	if (!ExecBeforeCommandTriggers(parsetree, cmdtag, per_command_context))
+		nb++;
+	nb += ExecInsteadOfCommandTriggers(parsetree, cmdtag, per_command_context);
+
+	/* Release working resources */
+	MemoryContextDelete(per_command_context);
+	return nb;
+}
+
+bool
+ExecBeforeCommandTriggers(Node *parsetree, const char *cmdtag,
+						  MemoryContext per_command_context)
+{
+	MemoryContext oldContext;
+	CommandContextData cmd;
+	RegProcedure *procs = list_triggers_for_command(cmdtag,
+													CMD_TRIGGER_FIRED_BEFORE);
+	RegProcedure proc;
+	int cur= 0;
+	bool cont = true;
+
+	/*
+	 * Do the functions evaluation in a per-command memory context, so that
+	 * leaked memory will be reclaimed once per command.
+	 */
+	oldContext = MemoryContextSwitchTo(per_command_context);
+	MemoryContextReset(per_command_context);
+
+	while (cont && InvalidOid != (proc = procs[cur++]))
+	{
+		if (cur==1)
+		{
+			cmd.tag = (char *)cmdtag;
+			pg_get_cmddef(&cmd, parsetree);
+		}
+		cont = call_cmdtrigger_procedure(proc, &cmd, per_command_context);
+
+		if (cont == false)
+			elog(WARNING,
+				 "command \"%s %s...\" was cancelled by procedure \"%s\"",
+				 cmdtag, cmd.objectname, get_func_name(proc));
+	}
+	MemoryContextSwitchTo(oldContext);
+	return cont;
+}
+
+/*
+ * return the count of triggers we fired
+ */
+int
+ExecInsteadOfCommandTriggers(Node *parsetree, const char *cmdtag,
+							 MemoryContext per_command_context)
+{
+	MemoryContext oldContext;
+	CommandContextData cmd;
+	RegProcedure *procs = list_triggers_for_command(cmdtag,
+													CMD_TRIGGER_FIRED_INSTEAD);
+	RegProcedure proc;
+	int cur = 0;
+
+	/*
+	 * Do the functions evaluation in a per-command memory context, so that
+	 * leaked memory will be reclaimed once per command.
+	 */
+	oldContext = MemoryContextSwitchTo(per_command_context);
+	MemoryContextReset(per_command_context);
+
+	while (InvalidOid != (proc = procs[cur++]))
+	{
+		if (cur==1)
+		{
+			cmd.tag = (char *)cmdtag;
+			pg_get_cmddef(&cmd, parsetree);
+		}
+		call_cmdtrigger_procedure(proc, &cmd, per_command_context);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+	return cur-1;
+}
+
+void
+ExecAfterCommandTriggers(Node *parsetree, const char *cmdtag)
+{
+	MemoryContext oldContext, per_command_context;
+	CommandContextData cmd;
+	RegProcedure *procs = list_triggers_for_command(cmdtag,
+													CMD_TRIGGER_FIRED_AFTER);
+	RegProcedure proc;
+	int cur = 0;
+
+	/*
+	 * Do the functions evaluation in a per-command memory context, so that
+	 * leaked memory will be reclaimed once per command.
+	 */
+	per_command_context =
+		AllocSetContextCreate(CurrentMemoryContext,
+							  "AfterTriggerCommandContext",
+							  ALLOCSET_DEFAULT_MINSIZE,
+							  ALLOCSET_DEFAULT_INITSIZE,
+							  ALLOCSET_DEFAULT_MAXSIZE);
+
+	oldContext = MemoryContextSwitchTo(per_command_context);
+
+	while (InvalidOid != (proc = procs[cur++]))
+	{
+		if (cur==1)
+		{
+			cmd.tag = (char *)cmdtag;
+			pg_get_cmddef(&cmd, parsetree);
+		}
+		call_cmdtrigger_procedure(proc, &cmd, per_command_context);
+	}
+
+	/* Release working resources */
+	MemoryContextSwitchTo(oldContext);
+	MemoryContextDelete(per_command_context);
+
+	return;
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c70a5bd..68034f6 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3451,6 +3451,58 @@ _copyCreateTrigStmt(CreateTrigStmt *from)
 	return newnode;
 }
 
+static DropPropertyStmt *
+_copyDropPropertyStmt(DropPropertyStmt *from)
+{
+	DropPropertyStmt *newnode = makeNode(DropPropertyStmt);
+
+	COPY_NODE_FIELD(relation);
+	COPY_STRING_FIELD(property);
+	COPY_SCALAR_FIELD(removeType);
+	COPY_SCALAR_FIELD(behavior);
+	COPY_SCALAR_FIELD(missing_ok);
+
+	return newnode;
+}
+
+static CreateCmdTrigStmt *
+_copyCreateCmdTrigStmt(CreateCmdTrigStmt *from)
+{
+	CreateCmdTrigStmt *newnode = makeNode(CreateCmdTrigStmt);
+
+	COPY_STRING_FIELD(command);
+	COPY_STRING_FIELD(trigname);
+	COPY_SCALAR_FIELD(timing);
+	COPY_NODE_FIELD(funcname);
+
+	return newnode;
+}
+
+static DropCmdTrigStmt *
+_copyDropCmdTrigStmt(DropCmdTrigStmt *from)
+{
+	DropCmdTrigStmt *newnode = makeNode(DropCmdTrigStmt);
+
+	COPY_STRING_FIELD(command);
+	COPY_STRING_FIELD(trigname);
+	COPY_SCALAR_FIELD(behavior);
+	COPY_SCALAR_FIELD(missing_ok);
+
+	return newnode;
+}
+
+static AlterCmdTrigStmt *
+_copyAlterCmdTrigStmt(AlterCmdTrigStmt *from)
+{
+	AlterCmdTrigStmt *newnode = makeNode(AlterCmdTrigStmt);
+
+	COPY_STRING_FIELD(command);
+	COPY_STRING_FIELD(trigname);
+	COPY_STRING_FIELD(tgenabled);
+
+	return newnode;
+}
+
 static CreatePLangStmt *
 _copyCreatePLangStmt(CreatePLangStmt *from)
 {
@@ -4303,6 +4355,18 @@ copyObject(void *from)
 		case T_CreateTrigStmt:
 			retval = _copyCreateTrigStmt(from);
 			break;
+		case T_DropPropertyStmt:
+			retval = _copyDropPropertyStmt(from);
+			break;
+		case T_CreateCmdTrigStmt:
+			retval = _copyCreateCmdTrigStmt(from);
+			break;
+		case T_DropCmdTrigStmt:
+			retval = _copyDropCmdTrigStmt(from);
+			break;
+		case T_AlterCmdTrigStmt:
+			retval = _copyAlterCmdTrigStmt(from);
+			break;
 		case T_CreatePLangStmt:
 			retval = _copyCreatePLangStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f490a7a..65535f9 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1773,6 +1773,50 @@ _equalCreateTrigStmt(CreateTrigStmt *a, CreateTrigStmt *b)
 }
 
 static bool
+_equalDropPropertyStmt(DropPropertyStmt *a, DropPropertyStmt *b)
+{
+	COMPARE_NODE_FIELD(relation);
+	COMPARE_STRING_FIELD(property);
+	COMPARE_SCALAR_FIELD(removeType);
+	COMPARE_SCALAR_FIELD(behavior);
+	COMPARE_SCALAR_FIELD(missing_ok);
+
+	return true;
+}
+
+static bool
+_equalCreateCmdTrigStmt(CreateCmdTrigStmt *a, CreateCmdTrigStmt *b)
+{
+	COMPARE_STRING_FIELD(command);
+	COMPARE_STRING_FIELD(trigname);
+	COMPARE_SCALAR_FIELD(timing);
+	COMPARE_NODE_FIELD(funcname);
+
+	return true;
+}
+
+static bool
+_equalDropCmdTrigStmt(DropCmdTrigStmt *a, DropCmdTrigStmt *b)
+{
+	COMPARE_STRING_FIELD(command);
+	COMPARE_STRING_FIELD(trigname);
+	COMPARE_SCALAR_FIELD(behavior);
+	COMPARE_SCALAR_FIELD(missing_ok);
+
+	return true;
+}
+
+static bool
+_equalAlterCmdTrigStmt(AlterCmdTrigStmt *a, AlterCmdTrigStmt *b)
+{
+	COMPARE_STRING_FIELD(command);
+	COMPARE_STRING_FIELD(trigname);
+	COMPARE_STRING_FIELD(tgenabled);
+
+	return true;
+}
+
+static bool
 _equalCreatePLangStmt(CreatePLangStmt *a, CreatePLangStmt *b)
 {
 	COMPARE_SCALAR_FIELD(replace);
@@ -2846,6 +2890,18 @@ equal(void *a, void *b)
 		case T_CreateTrigStmt:
 			retval = _equalCreateTrigStmt(a, b);
 			break;
+		case T_DropPropertyStmt:
+			retval = _equalDropPropertyStmt(a, b);
+			break;
+		case T_CreateCmdTrigStmt:
+			retval = _equalCreateCmdTrigStmt(a, b);
+			break;
+		case T_DropCmdTrigStmt:
+			retval = _equalDropCmdTrigStmt(a, b);
+			break;
+		case T_AlterCmdTrigStmt:
+			retval = _equalAlterCmdTrigStmt(a, b);
+			break;
 		case T_CreatePLangStmt:
 			retval = _equalCreatePLangStmt(a, b);
 			break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 31af47f..387043d 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1944,6 +1944,137 @@ _outPlannerParamItem(StringInfo str, PlannerParamItem *node)
  *****************************************************************************/
 
 static void
+_outInsertStmt(StringInfo str, InsertStmt *node)
+{
+	WRITE_NODE_TYPE("INSERTSTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(cols);
+	WRITE_NODE_FIELD(selectStmt);
+	WRITE_NODE_FIELD(returningList);
+	WRITE_NODE_FIELD(withClause);
+}
+
+static void
+_outDeleteStmt(StringInfo str, DeleteStmt *node)
+{
+	WRITE_NODE_TYPE("DELETESTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(usingClause);
+	WRITE_NODE_FIELD(whereClause);
+	WRITE_NODE_FIELD(returningList);
+	WRITE_NODE_FIELD(withClause);
+}
+
+static void
+_outUpdateStmt(StringInfo str, UpdateStmt *node)
+{
+	WRITE_NODE_TYPE("UPDATESTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(targetList);
+	WRITE_NODE_FIELD(whereClause);
+	WRITE_NODE_FIELD(fromClause);
+	WRITE_NODE_FIELD(returningList);
+	WRITE_NODE_FIELD(withClause);
+}
+
+static void
+_outAlterTableStmt(StringInfo str, AlterTableStmt *node)
+{
+	WRITE_NODE_TYPE("ALTERTABLESTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(cmds);
+	WRITE_CHAR_FIELD(relkind);
+}
+
+static void
+_outAlterTableCmd(StringInfo str, AlterTableCmd *node)
+{
+	WRITE_NODE_TYPE("ALTERTABLECMD");
+
+	WRITE_ENUM_FIELD(subtype, AlterTableType);
+	WRITE_STRING_FIELD(name);
+	WRITE_NODE_FIELD(def);
+	WRITE_ENUM_FIELD(behavior, DropBehavior);
+	WRITE_BOOL_FIELD(missing_ok);
+}
+
+static void
+_outAlterDomainStmt(StringInfo str, AlterDomainStmt *node)
+{
+	WRITE_NODE_TYPE("ALTERDOMAINSTMT");
+
+	WRITE_CHAR_FIELD(subtype);
+	WRITE_NODE_FIELD(typeName);
+	WRITE_STRING_FIELD(name);
+	WRITE_NODE_FIELD(def);
+	WRITE_ENUM_FIELD(behavior, DropBehavior);
+}
+
+static void
+_outGrantStmt(StringInfo str, GrantStmt *node)
+{
+	WRITE_NODE_TYPE("GRANTSTMT");
+
+	WRITE_BOOL_FIELD(is_grant);
+	WRITE_ENUM_FIELD(targtype, GrantTargetType);
+	WRITE_ENUM_FIELD(objtype, GrantObjectType);
+	WRITE_NODE_FIELD(objects);
+	WRITE_NODE_FIELD(privileges);
+	WRITE_NODE_FIELD(grantees);
+	WRITE_BOOL_FIELD(grant_option);
+	WRITE_ENUM_FIELD(behavior, DropBehavior);
+}
+
+static void
+_outGrantRoleStmt(StringInfo str, GrantRoleStmt *node)
+{
+	WRITE_NODE_TYPE("GRANTROLESTMT");
+
+	WRITE_NODE_FIELD(granted_roles);
+	WRITE_NODE_FIELD(grantee_roles);
+	WRITE_BOOL_FIELD(is_grant);
+	WRITE_BOOL_FIELD(admin_opt);
+	WRITE_STRING_FIELD(grantor);
+	WRITE_ENUM_FIELD(behavior, DropBehavior);
+}
+
+static void
+_outAlterDefaultPrivilegesStmt(StringInfo str, AlterDefaultPrivilegesStmt *node)
+{
+	WRITE_NODE_TYPE("ALTERDEFAULTPRIVILEGESSTMT");
+
+	WRITE_NODE_FIELD(options);
+	WRITE_NODE_FIELD(action);
+}
+
+static void
+_outClusterStmt(StringInfo str, ClusterStmt *node)
+{
+	WRITE_NODE_TYPE("CLUSTERSTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_STRING_FIELD(indexname);
+	WRITE_BOOL_FIELD(verbose);
+}
+
+static void
+_outCopyStmt(StringInfo str, CopyStmt *node)
+{
+	WRITE_NODE_TYPE("COPYSTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(query);
+	WRITE_NODE_FIELD(attlist);
+	WRITE_BOOL_FIELD(is_from);
+	WRITE_STRING_FIELD(filename);
+	WRITE_NODE_FIELD(options);
+}
+
+static void
 _outCreateStmt(StringInfo str, CreateStmt *node)
 {
 	WRITE_NODE_TYPE("CREATESTMT");
@@ -1960,6 +2091,31 @@ _outCreateStmt(StringInfo str, CreateStmt *node)
 }
 
 static void
+_outDefineStmt(StringInfo str, DefineStmt *node)
+{
+	WRITE_NODE_TYPE("DEFINESTMT");
+
+	WRITE_ENUM_FIELD(kind, ObjectType);
+	WRITE_BOOL_FIELD(oldstyle);
+	WRITE_NODE_FIELD(defnames);
+	WRITE_NODE_FIELD(args);
+	WRITE_NODE_FIELD(definition);
+}
+
+static void
+_outAlterTSConfigurationStmt(StringInfo str, AlterTSConfigurationStmt *node)
+{
+	WRITE_NODE_TYPE("ALTERTSCONFIGURATIONSTMT");
+
+	WRITE_NODE_FIELD(cfgname);
+	WRITE_NODE_FIELD(tokentype);
+	WRITE_NODE_FIELD(dicts);
+	WRITE_BOOL_FIELD(override);
+	WRITE_BOOL_FIELD(replace);
+	WRITE_BOOL_FIELD(missing_ok);
+}
+
+static void
 _outCreateForeignTableStmt(StringInfo str, CreateForeignTableStmt *node)
 {
 	WRITE_NODE_TYPE("CREATEFOREIGNTABLESTMT");
@@ -1971,6 +2127,28 @@ _outCreateForeignTableStmt(StringInfo str, CreateForeignTableStmt *node)
 }
 
 static void
+_outDropStmt(StringInfo str, DropStmt *node)
+{
+	WRITE_NODE_TYPE("DROPSTMT");
+
+	WRITE_NODE_FIELD(objects);
+	WRITE_ENUM_FIELD(removeType,ObjectType);
+	WRITE_ENUM_FIELD(behavior,DropBehavior);
+	WRITE_BOOL_FIELD(missing_ok);
+}
+
+static void
+_outCommentStmt(StringInfo str, CommentStmt *node)
+{
+	WRITE_NODE_TYPE("COMMENTSTMT");
+
+	WRITE_ENUM_FIELD(objtype, ObjectType);
+	WRITE_NODE_FIELD(objname);
+	WRITE_NODE_FIELD(objargs);
+	WRITE_STRING_FIELD(comment);
+}
+
+static void
 _outIndexStmt(StringInfo str, IndexStmt *node)
 {
 	WRITE_NODE_TYPE("INDEXSTMT");
@@ -1994,6 +2172,33 @@ _outIndexStmt(StringInfo str, IndexStmt *node)
 }
 
 static void
+_outCreateFunctionStmt(StringInfo str, CreateFunctionStmt *node)
+{
+	WRITE_NODE_TYPE("CREATEFUNCTIONSTMT");
+
+	WRITE_BOOL_FIELD(replace);
+	WRITE_NODE_FIELD(funcname);
+	WRITE_NODE_FIELD(parameters);
+	WRITE_NODE_FIELD(returnType);
+	WRITE_NODE_FIELD(options);
+	WRITE_NODE_FIELD(withClause);
+}
+
+static void
+_outRuleStmt(StringInfo str, RuleStmt *node)
+{
+	WRITE_NODE_TYPE("RULESTMT");
+
+	WRITE_NODE_FIELD(relation);
+	WRITE_STRING_FIELD(rulename);
+	WRITE_NODE_FIELD(whereClause);
+	WRITE_ENUM_FIELD(event, CmdType);
+	WRITE_BOOL_FIELD(instead);
+	WRITE_NODE_FIELD(actions);
+	WRITE_BOOL_FIELD(replace);
+}
+
+static void
 _outNotifyStmt(StringInfo str, NotifyStmt *node)
 {
 	WRITE_NODE_TYPE("NOTIFY");
@@ -2003,6 +2208,95 @@ _outNotifyStmt(StringInfo str, NotifyStmt *node)
 }
 
 static void
+_outViewStmt(StringInfo str, ViewStmt *node)
+{
+	WRITE_NODE_TYPE("VIEWSTMT");
+
+	WRITE_NODE_FIELD(view);
+	WRITE_NODE_FIELD(aliases);
+	WRITE_NODE_FIELD(query);
+	WRITE_BOOL_FIELD(replace);
+}
+
+static void
+_outCreateDomainStmt(StringInfo str, CreateDomainStmt *node)
+{
+	WRITE_NODE_TYPE("CREATECONVERSIONSTMT");
+
+	WRITE_NODE_FIELD(domainname);
+	WRITE_NODE_FIELD(typeName);
+	WRITE_NODE_FIELD(collClause);
+	WRITE_NODE_FIELD(constraints);
+}
+
+static void
+_outCreatedbStmt(StringInfo str, CreatedbStmt *node)
+{
+	WRITE_NODE_TYPE("CREATEDBSTMT");
+
+	WRITE_STRING_FIELD(dbname);
+	WRITE_NODE_FIELD(options);
+}
+
+static void
+_outVacuumStmt(StringInfo str, VacuumStmt *node)
+{
+	WRITE_NODE_TYPE("VACUUMSTMT");
+
+	WRITE_INT_FIELD(options);
+	WRITE_INT_FIELD(freeze_min_age);
+	WRITE_INT_FIELD(freeze_table_age);
+	WRITE_NODE_FIELD(relation);
+	WRITE_NODE_FIELD(va_cols);
+}
+
+static void
+_outVariableSetStmt(StringInfo str, VariableSetStmt *node)
+{
+	WRITE_NODE_TYPE("VARIABLESETSTMT");
+
+	WRITE_ENUM_FIELD(kind, VariableSetKind);
+	WRITE_STRING_FIELD(name);
+	WRITE_NODE_FIELD(args);
+	WRITE_BOOL_FIELD(is_local);
+}
+
+static void
+_outCreatePLangStmt(StringInfo str, CreatePLangStmt *node)
+{
+	WRITE_NODE_TYPE("CREATESPLANGSTMT");
+
+	WRITE_BOOL_FIELD(replace);
+	WRITE_STRING_FIELD(plname);
+	WRITE_NODE_FIELD(plhandler);
+	WRITE_NODE_FIELD(plinline);
+	WRITE_NODE_FIELD(plvalidator);
+	WRITE_BOOL_FIELD(pltrusted);
+}
+
+static void
+_outCreateSchemaStmt(StringInfo str, CreateSchemaStmt *node)
+{
+	WRITE_NODE_TYPE("CREATESCHEMASTMT");
+
+	WRITE_STRING_FIELD(schemaname);
+	WRITE_STRING_FIELD(authid);
+	WRITE_NODE_FIELD(schemaElts);
+}
+
+static void
+_outCreateConversionStmt(StringInfo str, CreateConversionStmt *node)
+{
+	WRITE_NODE_TYPE("CREATECONVERSIONSTMT");
+
+	WRITE_NODE_FIELD(conversion_name);
+	WRITE_STRING_FIELD(for_encoding_name);
+	WRITE_STRING_FIELD(to_encoding_name);
+	WRITE_NODE_FIELD(func_name);
+	WRITE_BOOL_FIELD(def);
+}
+
+static void
 _outDeclareCursorStmt(StringInfo str, DeclareCursorStmt *node)
 {
 	WRITE_NODE_TYPE("DECLARECURSOR");
@@ -2064,6 +2358,23 @@ _outDefElem(StringInfo str, DefElem *node)
 }
 
 static void
+_outPrivGrantee(StringInfo str, PrivGrantee *node)
+{
+	WRITE_NODE_TYPE("PRIVGRANTEE");
+
+	WRITE_STRING_FIELD(rolname);
+}
+
+static void
+_outAccessPriv(StringInfo str, AccessPriv *node)
+{
+	WRITE_NODE_TYPE("ACCESSPRIV");
+
+	WRITE_STRING_FIELD(priv_name);
+	WRITE_NODE_FIELD(cols);
+}
+
+static void
 _outInhRelation(StringInfo str, InhRelation *node)
 {
 	WRITE_NODE_TYPE("INHRELATION");
@@ -2094,6 +2405,16 @@ _outXmlSerialize(StringInfo str, XmlSerialize *node)
 }
 
 static void
+_outCreateExtensionStmt(StringInfo str, CreateExtensionStmt *node)
+{
+	WRITE_NODE_TYPE("CREATEEXTENSIONSTMT");
+
+	WRITE_STRING_FIELD(extname);
+	WRITE_BOOL_FIELD(if_not_exists);
+	WRITE_NODE_FIELD(options);
+}
+
+static void
 _outColumnDef(StringInfo str, ColumnDef *node)
 {
 	WRITE_NODE_TYPE("COLUMNDEF");
@@ -2251,6 +2572,17 @@ _outWindowClause(StringInfo str, WindowClause *node)
 }
 
 static void
+_outFunctionParameter(StringInfo str, FunctionParameter *node)
+{
+	WRITE_NODE_TYPE("FUNCTIONPARAMETER");
+
+	WRITE_STRING_FIELD(name);
+	WRITE_NODE_FIELD(argType);
+	WRITE_CHAR_FIELD(mode);
+	WRITE_NODE_FIELD(defexpr);
+}
+
+static void
 _outRowMarkClause(StringInfo str, RowMarkClause *node)
 {
 	WRITE_NODE_TYPE("ROWMARKCLAUSE");
@@ -2358,6 +2690,16 @@ _outRangeTblEntry(StringInfo str, RangeTblEntry *node)
 }
 
 static void
+_outAlterCmdTrigStmt(StringInfo str, AlterCmdTrigStmt *node)
+{
+	WRITE_NODE_TYPE("ALTERCMDTRIGSTMT");
+
+	WRITE_STRING_FIELD(command);
+	WRITE_STRING_FIELD(trigname);
+	WRITE_STRING_FIELD(tgenabled);
+}
+
+static void
 _outAExpr(StringInfo str, A_Expr *node)
 {
 	WRITE_NODE_TYPE("AEXPR");
@@ -3034,24 +3376,102 @@ _outNode(StringInfo str, void *obj)
 				_outPlannerParamItem(str, obj);
 				break;
 
+			case T_InsertStmt:
+				_outInsertStmt(str, obj);
+				break;
+			case T_DeleteStmt:
+				_outDeleteStmt(str, obj);
+				break;
+			case T_UpdateStmt:
+				_outUpdateStmt(str, obj);
+				break;
+			case T_AlterTableStmt:
+				_outAlterTableStmt(str, obj);
+				break;
+			case T_AlterTableCmd:
+				_outAlterTableCmd(str, obj);
+				break;
+			case T_AlterDomainStmt:
+				_outAlterDomainStmt(str, obj);
+				break;
+			case T_GrantStmt:
+				_outGrantStmt(str, obj);
+				break;
+			case T_GrantRoleStmt:
+				_outGrantRoleStmt(str, obj);
+				break;
+			case T_AlterDefaultPrivilegesStmt:
+				_outAlterDefaultPrivilegesStmt(str, obj);
+				break;
+			case T_ClusterStmt:
+				_outClusterStmt(str, obj);
+				break;
+			case T_CopyStmt:
+				_outCopyStmt(str, obj);
+				break;
 			case T_CreateStmt:
 				_outCreateStmt(str, obj);
 				break;
+			case T_DefineStmt:
+				_outDefineStmt(str, obj);
+				break;
+			case T_AlterTSConfigurationStmt:
+				_outAlterTSConfigurationStmt(str, obj);
+				break;
 			case T_CreateForeignTableStmt:
 				_outCreateForeignTableStmt(str, obj);
 				break;
+			case T_DropStmt:
+				_outDropStmt(str, obj);
+				break;
+			case T_CommentStmt:
+				_outCommentStmt(str, obj);
+				break;
 			case T_IndexStmt:
 				_outIndexStmt(str, obj);
 				break;
+			case T_CreateFunctionStmt:
+				_outCreateFunctionStmt(str, obj);
+				break;
+			case T_RuleStmt:
+				_outRuleStmt(str, obj);
+				break;
 			case T_NotifyStmt:
 				_outNotifyStmt(str, obj);
 				break;
+			case T_ViewStmt:
+				_outViewStmt(str, obj);
+				break;
+			case T_CreateDomainStmt:
+				_outCreateDomainStmt(str, obj);
+				break;
+			case T_CreatedbStmt:
+				_outCreatedbStmt(str, obj);
+				break;
+			case T_VacuumStmt:
+				_outVacuumStmt(str, obj);
+				break;
+			case T_VariableSetStmt:
+				_outVariableSetStmt(str, obj);
+				break;
+			case T_CreatePLangStmt:
+				_outCreatePLangStmt(str, obj);
+				break;
+			case T_CreateSchemaStmt:
+				_outCreateSchemaStmt(str, obj);
+				break;
+			case T_CreateConversionStmt:
+				_outCreateConversionStmt(str, obj);
+				break;
 			case T_DeclareCursorStmt:
 				_outDeclareCursorStmt(str, obj);
 				break;
 			case T_SelectStmt:
 				_outSelectStmt(str, obj);
 				break;
+			case T_CreateExtensionStmt:
+				_outCreateExtensionStmt(str, obj);
+				break;
 			case T_ColumnDef:
 				_outColumnDef(str, obj);
 				break;
@@ -3076,6 +3496,9 @@ _outNode(StringInfo str, void *obj)
 			case T_WindowClause:
 				_outWindowClause(str, obj);
 				break;
+			case T_FunctionParameter:
+				_outFunctionParameter(str, obj);
+				break;
 			case T_RowMarkClause:
 				_outRowMarkClause(str, obj);
 				break;
@@ -3091,6 +3514,9 @@ _outNode(StringInfo str, void *obj)
 			case T_RangeTblEntry:
 				_outRangeTblEntry(str, obj);
 				break;
+			case T_AlterCmdTrigStmt:
+				_outAlterCmdTrigStmt(str, obj);
+				break;
 			case T_A_Expr:
 				_outAExpr(str, obj);
 				break;
@@ -3139,6 +3565,12 @@ _outNode(StringInfo str, void *obj)
 			case T_DefElem:
 				_outDefElem(str, obj);
 				break;
+			case T_PrivGrantee:
+				_outPrivGrantee(str, obj);
+				break;
+			case T_AccessPriv:
+				_outAccessPriv(str, obj);
+				break;
 			case T_InhRelation:
 				_outInhRelation(str, obj);
 				break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3de20ad..2b1a76d 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -255,6 +255,203 @@ _readDeclareCursorStmt(void)
 }
 
 /*
+ * _readCreateStmt
+ */
+static CreateStmt *
+_readCreateStmt(void)
+{
+	READ_LOCALS(CreateStmt);
+
+	READ_NODE_FIELD(relation);
+	READ_NODE_FIELD(tableElts);
+	READ_NODE_FIELD(inhRelations);
+	READ_NODE_FIELD(ofTypename);
+	READ_NODE_FIELD(constraints);
+	READ_NODE_FIELD(options);
+	READ_ENUM_FIELD(oncommit, OnCommitAction);
+	READ_STRING_FIELD(tablespacename);
+	READ_BOOL_FIELD(if_not_exists);
+
+	READ_DONE();
+}
+
+/*
+ * _readDropStmt
+ */
+static DropStmt *
+_readDropStmt(void)
+{
+	READ_LOCALS(DropStmt);
+
+	READ_NODE_FIELD(objects);
+	READ_ENUM_FIELD(removeType,ObjectType);
+	READ_ENUM_FIELD(behavior,DropBehavior);
+	READ_BOOL_FIELD(missing_ok);
+
+	READ_DONE();
+}
+
+/*
+ * _readCreateExtensionStmt
+ */
+static CreateExtensionStmt *
+_readCreateExtensionStmt(void)
+{
+	READ_LOCALS(CreateExtensionStmt);
+
+	READ_STRING_FIELD(extname);
+	READ_BOOL_FIELD(if_not_exists);
+	READ_NODE_FIELD(options);
+
+	READ_DONE();
+}
+
+/*
+ * _readAlterCmdTrigStmt
+ */
+static AlterCmdTrigStmt *
+_readAlterCmdTrigStmt(void)
+{
+	READ_LOCALS(AlterCmdTrigStmt);
+
+	READ_STRING_FIELD(command);
+	READ_STRING_FIELD(trigname);
+	READ_CHAR_FIELD(tgenabled);
+
+	READ_DONE();
+}
+
+/*
+ * _readTypeName
+ */
+static TypeName *
+_readTypeName(void)
+{
+	READ_LOCALS(TypeName);
+
+	READ_NODE_FIELD(names);
+	READ_OID_FIELD(typeOid);
+	READ_BOOL_FIELD(setof);
+	READ_BOOL_FIELD(pct_type);
+	READ_NODE_FIELD(typmods);
+	READ_INT_FIELD(typemod);
+	READ_NODE_FIELD(arrayBounds);
+	READ_LOCATION_FIELD(location);
+
+	READ_DONE();
+}
+
+/*
+ * _readColumnDef
+ */
+static ColumnDef *
+_readColumnDef(void)
+{
+	READ_LOCALS(ColumnDef);
+
+	READ_STRING_FIELD(colname);
+	READ_NODE_FIELD(typeName);
+	READ_INT_FIELD(inhcount);
+	READ_BOOL_FIELD(is_local);
+	READ_BOOL_FIELD(is_not_null);
+	READ_BOOL_FIELD(is_from_type);
+	READ_CHAR_FIELD(storage);
+	READ_NODE_FIELD(raw_default);
+	READ_NODE_FIELD(cooked_default);
+	READ_NODE_FIELD(collClause);
+	READ_OID_FIELD(collOid);
+	READ_NODE_FIELD(constraints);
+	READ_NODE_FIELD(fdwoptions);
+
+	READ_DONE();
+}
+
+/*
+ * _readConstraint
+ */
+static Constraint *
+_readConstraint(void)
+{
+	READ_LOCALS(Constraint);
+
+	READ_STRING_FIELD(conname);
+	READ_BOOL_FIELD(deferrable);
+	READ_BOOL_FIELD(initdeferred);
+	READ_LOCATION_FIELD(location);
+
+	/* READ_ENUM_FIELD(contype,ConstrType); */
+
+	token = pg_strtok(&length); /* skip :constraint */
+	token = pg_strtok(&length); /* get field value */
+
+	if (strncmp(token, "NULL", 4) == 0)
+		local_node->contype = CONSTR_NULL;
+	else if (strncmp(token, "NOT_NULL", 8) == 0)
+		local_node->contype = CONSTR_NOTNULL;
+	else if (strncmp(token, "DEFAULT", 7) == 0)
+	{
+		local_node->contype = CONSTR_DEFAULT;
+		READ_NODE_FIELD(raw_expr);
+		READ_STRING_FIELD(cooked_expr);
+	}
+	else if (strncmp(token, "CHECK", 7) == 0)
+	{
+		local_node->contype = CONSTR_CHECK;
+		READ_NODE_FIELD(raw_expr);
+		READ_STRING_FIELD(cooked_expr);
+	}
+	else if (strncmp(token, "PRIMARY_KEY", 11) == 0)
+	{
+		local_node->contype = CONSTR_PRIMARY;
+		READ_NODE_FIELD(keys);
+		READ_NODE_FIELD(options);
+		READ_STRING_FIELD(indexname);
+		READ_STRING_FIELD(indexspace);
+	}
+	else if (strncmp(token, "UNIQUE", 6) == 0)
+	{
+		local_node->contype = CONSTR_UNIQUE;
+		READ_NODE_FIELD(keys);
+		READ_NODE_FIELD(options);
+		READ_STRING_FIELD(indexname);
+		READ_STRING_FIELD(indexspace);
+	}
+	else if (strncmp(token, "EXCLUSION", 9) == 0)
+	{
+		local_node->contype = CONSTR_EXCLUSION;
+		READ_NODE_FIELD(exclusions);
+		READ_NODE_FIELD(keys);
+		READ_NODE_FIELD(options);
+		READ_STRING_FIELD(indexname);
+		READ_STRING_FIELD(indexspace);
+	}
+	else if (strncmp(token, "FOREIGN_KEY", 11) == 0)
+	{
+		local_node->contype = CONSTR_FOREIGN;
+		READ_NODE_FIELD(pktable);
+		READ_NODE_FIELD(fk_attrs);
+		READ_NODE_FIELD(pk_attrs);
+		READ_CHAR_FIELD(fk_matchtype);
+		READ_CHAR_FIELD(fk_upd_action);
+		READ_CHAR_FIELD(fk_del_action);
+		READ_BOOL_FIELD(skip_validation);
+		READ_BOOL_FIELD(initially_valid);
+	}
+	else if (strncmp(token, "ATTR_DEFERRABLE", 15) == 0)
+		local_node->contype = CONSTR_ATTR_DEFERRABLE;
+	else if (strncmp(token, "ATTR_NOT_DEFERRABLE", 19) == 0)
+		local_node->contype = CONSTR_ATTR_NOT_DEFERRABLE;
+	else if (strncmp(token, "ATTR_DEFERRED", 13) == 0)
+		local_node->contype = CONSTR_ATTR_DEFERRED;
+	else if (strncmp(token, "ATTR_IMMEDIATE", 14) == 0)
+		local_node->contype = CONSTR_ATTR_IMMEDIATE;
+	else
+		elog(ERROR, "unrecognized constraint type: %d",
+			 (int) local_node->contype);
+	READ_DONE();
+}
+
+/*
  * _readSortGroupClause
  */
 static SortGroupClause *
@@ -1254,6 +1451,20 @@ parseNodeString(void)
 
 	if (MATCH("QUERY", 5))
 		return_value = _readQuery();
+	else if (MATCH("CREATESTMT", 10))
+		return_value = _readCreateStmt();
+	else if (MATCH("DROPSTMT", 8))
+		return_value = _readDropStmt();
+	else if (MATCH("CREATEEXTENSIONSTMT", 19))
+		return_value = _readCreateExtensionStmt();
+	else if (MATCH("ALTERCMDTRIGSTMT", 16))
+		return_value = _readAlterCmdTrigStmt();
+	else if (MATCH("TYPENAME", 8))
+		return_value = _readTypeName();
+	else if (MATCH("COLUMNDEF", 9))
+		return_value = _readColumnDef();
+	else if (MATCH("CONSTRAINT", 10))
+		return_value = _readConstraint();
 	else if (MATCH("SORTGROUPCLAUSE", 15))
 		return_value = _readSortGroupClause();
 	else if (MATCH("WINDOWCLAUSE", 12))
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2a497d1..1ecd0ce 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -195,6 +195,7 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 }
 
 %type <node>	stmt schema_stmt
+		AlterCmdTrigStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectSchemaStmt AlterOwnerStmt AlterSeqStmt AlterTableStmt
@@ -208,12 +209,12 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
 		CreateSchemaStmt CreateSeqStmt CreateStmt CreateTableSpaceStmt
 		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
-		CreateAssertStmt CreateTrigStmt
+		CreateAssertStmt CreateTrigStmt CreateCmdTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
 		DropGroupStmt DropOpClassStmt DropOpFamilyStmt DropPLangStmt DropStmt
-		DropAssertStmt DropTrigStmt DropRuleStmt DropCastStmt DropRoleStmt
-		DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt
+		DropAssertStmt DropTrigStmt DropCmdTrigStmt DropRuleStmt DropCastStmt
+		DropRoleStmt DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt
 		DropForeignServerStmt DropUserMappingStmt ExplainStmt FetchStmt
 		GrantStmt GrantRoleStmt IndexStmt InsertStmt ListenStmt LoadStmt
 		LockStmt NotifyStmt ExplainableStmt PreparableStmt
@@ -268,6 +269,7 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 %type <list>	TriggerEvents TriggerOneEvent
 %type <value>	TriggerFuncArg
 %type <node>	TriggerWhen
+%type <str>		trigger_command enable_trigger
 
 %type <str>		copy_file_name
 				database_name access_method_clause access_method attr_name
@@ -495,7 +497,7 @@ static void processCASbits(int cas_bits, int location, const char *constrType,
 
 	CACHE CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
 	CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
-	CLUSTER COALESCE COLLATE COLLATION COLUMN COMMENT COMMENTS COMMIT
+	CLUSTER COALESCE COLLATE COLLATION COLUMN COMMAND COMMENT COMMENTS COMMIT
 	COMMITTED CONCURRENTLY CONFIGURATION CONNECTION CONSTRAINT CONSTRAINTS
 	CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE
 	CROSS CSV CURRENT_P
@@ -675,7 +677,8 @@ stmtmulti:	stmtmulti ';' stmt
 		;
 
 stmt :
-			AlterDatabaseStmt
+			AlterCmdTrigStmt
+			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
 			| AlterDomainStmt
@@ -726,6 +729,7 @@ stmt :
 			| CreateStmt
 			| CreateTableSpaceStmt
 			| CreateTrigStmt
+			| CreateCmdTrigStmt
 			| CreateRoleStmt
 			| CreateUserStmt
 			| CreateUserMappingStmt
@@ -749,6 +753,7 @@ stmt :
 			| DropStmt
 			| DropTableSpaceStmt
 			| DropTrigStmt
+			| DropCmdTrigStmt
 			| DropRoleStmt
 			| DropUserStmt
 			| DropUserMappingStmt
@@ -4198,6 +4203,83 @@ DropTrigStmt:
 /*****************************************************************************
  *
  *		QUERIES :
+ *				CREATE TRIGGER ... BEFORE|INSTEAD OF|AFTER COMMAND ...
+ *				DROP TRIGGER ... ON COMMAND ...
+ *
+ *****************************************************************************/
+
+CreateCmdTrigStmt:
+			CREATE TRIGGER name TriggerActionTime COMMAND trigger_command
+			EXECUTE PROCEDURE func_name '(' TriggerFuncArgs ')'
+				{
+					CreateCmdTrigStmt *n = makeNode(CreateCmdTrigStmt);
+					n->trigname = $3;
+					n->timing   = $4;
+					n->command  = $6;
+					n->funcname = $9;
+					$$ = (Node *)n;
+				}
+		;
+
+/*
+ * that will get matched against what CreateCommandTag  returns
+ *
+ * we don't support Command Triggers on every possible command that PostgreSQL
+ * supports, this list should match with the implementation of rewriting
+ * utility statements in pg_get_cmddef() in src/backend/utils/adt/ruleutils.c
+ */
+trigger_command:
+			CREATE TABLE						{ $$ = "CREATE TABLE"; }
+			| ALTER TABLE						{ $$ = "ALTER TABLE"; }
+			| DROP TABLE						{ $$ = "DROP TABLE"; }
+			| CREATE VIEW						{ $$ = "CREATE VIEW"; }
+			| DROP VIEW							{ $$ = "DROP VIEW"; }
+			| CREATE EXTENSION					{ $$ = "CREATE EXTENSION"; }
+			| DROP EXTENSION					{ $$ = "DROP EXTENSION"; }
+		;
+
+DropCmdTrigStmt:
+			DROP TRIGGER name ON COMMAND trigger_command opt_drop_behavior
+				{
+					DropCmdTrigStmt *n = makeNode(DropCmdTrigStmt);
+					n->trigname = $3;
+					n->command  = $6;
+					n->behavior = $7;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| DROP TRIGGER IF_P EXISTS name ON COMMAND trigger_command opt_drop_behavior
+				{
+					DropCmdTrigStmt *n = makeNode(DropCmdTrigStmt);
+					n->trigname = $5;
+					n->command  = $8;
+					n->behavior = $9;
+					n->missing_ok = true;
+					$$ = (Node *) n;
+				}
+		;
+
+AlterCmdTrigStmt:
+			ALTER TRIGGER name ON COMMAND trigger_command SET enable_trigger
+				{
+					AlterCmdTrigStmt *n = makeNode(AlterCmdTrigStmt);
+					n->trigname   = $3;
+					n->command    = $6;
+					n->tgenabled  = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+enable_trigger:
+			ENABLE_P					{ $$ = "O"; }
+			| ENABLE_P REPLICA			{ $$ = "R"; }
+			| ENABLE_P ALWAYS			{ $$ = "A"; }
+			| DISABLE_P					{ $$ = "D"; }
+		;
+
+/*****************************************************************************
+ *
+ *		QUERIES :
  *				CREATE ASSERTION ...
  *				DROP ASSERTION ...
  *
@@ -6559,6 +6641,15 @@ RenameStmt: ALTER AGGREGATE func_name aggr_args RENAME TO name
 					n->newname = $8;
 					$$ = (Node *)n;
 				}
+			| ALTER TRIGGER name ON COMMAND trigger_command RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_CMDTRIGGER;
+					n->object  = list_make1(makeString($6));
+					n->subname = $3;
+					n->newname = $9;
+					$$ = (Node *)n;
+				}
 			| ALTER ROLE RoleId RENAME TO RoleId
 				{
 					RenameStmt *n = makeNode(RenameStmt);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 6f88c47..ef80550 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -25,6 +25,7 @@
 #include "commands/alter.h"
 #include "commands/async.h"
 #include "commands/cluster.h"
+#include "commands/cmdtrigger.h"
 #include "commands/comment.h"
 #include "commands/collationcmds.h"
 #include "commands/conversioncmds.h"
@@ -184,6 +185,8 @@ check_xact_readonly(Node *parsetree)
 		case T_CommentStmt:
 		case T_DefineStmt:
 		case T_CreateCastStmt:
+		case T_CreateCmdTrigStmt:
+		case T_AlterCmdTrigStmt:
 		case T_CreateConversionStmt:
 		case T_CreatedbStmt:
 		case T_CreateDomainStmt:
@@ -205,6 +208,7 @@ check_xact_readonly(Node *parsetree)
 		case T_CreateRangeStmt:
 		case T_AlterEnumStmt:
 		case T_ViewStmt:
+		case T_DropCmdTrigStmt:
 		case T_DropStmt:
 		case T_DropdbStmt:
 		case T_DropTableSpaceStmt:
@@ -321,8 +325,17 @@ ProcessUtility(Node *parsetree,
 			   DestReceiver *dest,
 			   char *completionTag)
 {
+	/* we want a completion tag to identify which triggers to run, and that's
+	 * true whatever is given as completionTag here, so just call
+	 * CreateCommandTag() for our own business.
+	 */
+	const char *commandTag = CreateCommandTag(parsetree);
+
 	Assert(queryString != NULL);	/* required as of 8.4 */
 
+	if (ExecBeforeOrInsteadOfCommandTriggers(parsetree, commandTag) > 0)
+		return;
+
 	/*
 	 * We provide a function hook variable that lets loadable plugins get
 	 * control when ProcessUtility is called.  Such a plugin would normally
@@ -334,6 +347,8 @@ ProcessUtility(Node *parsetree,
 	else
 		standard_ProcessUtility(parsetree, queryString, params,
 								isTopLevel, dest, completionTag);
+
+	ExecAfterCommandTriggers(parsetree, commandTag);
 }
 
 void
@@ -1040,6 +1055,18 @@ standard_ProcessUtility(Node *parsetree,
 								 InvalidOid, InvalidOid, false);
 			break;
 
+		case T_CreateCmdTrigStmt:
+			(void) CreateCmdTrigger((CreateCmdTrigStmt *) parsetree, queryString);
+			break;
+
+		case T_DropCmdTrigStmt:
+			DropCmdTrigger((DropCmdTrigStmt *) parsetree);
+			break;
+
+		case T_AlterCmdTrigStmt:
+			(void) AlterCmdTrigger((AlterCmdTrigStmt *) parsetree);
+			break;
+
 		case T_CreatePLangStmt:
 			CreateProceduralLanguage((CreatePLangStmt *) parsetree);
 			break;
@@ -1931,6 +1958,18 @@ CreateCommandTag(Node *parsetree)
 			tag = "CREATE TRIGGER";
 			break;
 
+		case T_CreateCmdTrigStmt:
+			tag = "CREATE COMMAND TRIGGER";
+			break;
+
+		case T_DropCmdTrigStmt:
+			tag = "DROP COMMAND TRIGGER";
+			break;
+
+		case T_AlterCmdTrigStmt:
+			tag = "ALTER COMMAND TRIGGER";
+			break;
+
 		case T_CreatePLangStmt:
 			tag = "CREATE LANGUAGE";
 			break;
@@ -2132,6 +2171,13 @@ CreateCommandTag(Node *parsetree)
 			break;
 	}
 
+	/*
+	 * Useful to raise WARNINGs for any DDL command not yet supported.
+	 *
+	elog(WARNING, "Command Tag:    %s", tag);
+	elog(WARNING, "Note to String: %s", nodeToString(parsetree));
+	 */
+
 	return tag;
 }
 
@@ -2426,6 +2472,22 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_DropPropertyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_CreateCmdTrigStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_DropCmdTrigStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterCmdTrigStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		case T_CreatePLangStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 75923a6..83775bf 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -39,9 +39,11 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/clauses.h"
 #include "optimizer/tlist.h"
+#include "parser/analyze.h"
 #include "parser/keywords.h"
 #include "parser/parse_func.h"
 #include "parser/parse_oper.h"
+#include "parser/parse_type.h"
 #include "parser/parser.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteHandler.h"
@@ -256,7 +258,6 @@ static char *flatten_reloptions(Oid relid);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
-
 /* ----------
  * get_ruledef			- Do it all and return a text
  *				  that could be used as a statement
@@ -7326,3 +7327,614 @@ flatten_reloptions(Oid relid)
 
 	return result;
 }
+
+/*
+ * Functions that ouputs a COMMAND given a Utility parsetree
+ *
+ * FIXME: First some tools that I couldn't find in the sources.
+ */
+static char *
+RangeVarToString(RangeVar *r)
+{
+	char *qualified_name = (char *)palloc0(NAMEDATALEN*2+2);
+
+	sprintf(qualified_name, "%s%s%s",
+			r->schemaname == NULL? "": r->schemaname,
+			r->schemaname == NULL? "": ".",
+			r->relname);
+
+	return qualified_name;
+}
+
+static const char *
+relkindToString(ObjectType relkind)
+{
+	const char *kind;
+
+	switch (relkind)
+	{
+		case OBJECT_FOREIGN_TABLE:
+			kind = "FOREIGN TABLE";
+			break;
+
+		case OBJECT_INDEX:
+			kind = "INDEX";
+			break;
+
+		case OBJECT_SEQUENCE:
+			kind = "SEQUENCE";
+			break;
+
+		case OBJECT_TABLE:
+			kind = "TABLE";
+			break;
+
+		case OBJECT_VIEW:
+			kind = "VIEW";
+			break;
+
+		default:
+			elog(DEBUG2, "unrecognized relkind: %d", relkind);
+			break;
+	}
+	return kind;
+}
+
+static void
+_maybeAddSeparator(StringInfo buf, const char *sep, bool *first)
+{
+	if (*first) *first = false;
+	else        appendStringInfoString(buf, sep);
+}
+
+/*
+ * The DROP statement is "generic" as in supporting multiple object types. The
+ * specialized part is only finding the names of the objects dropped.
+ *
+ * Also the easiest way to get the command prefix is to use the command tag.
+ */
+static void
+_rwDropStmt(CommandContext cmd, DropStmt *node)
+{
+	StringInfoData buf;
+	ListCell *obj;
+	bool first = true;
+
+	initStringInfo(&buf);
+	appendStringInfo(&buf, "%s ", cmd->tag);
+
+	foreach(obj, node->objects)
+	{
+		switch (node->removeType)
+		{
+			case OBJECT_TABLE:
+			case OBJECT_SEQUENCE:
+			case OBJECT_VIEW:
+			case OBJECT_INDEX:
+			case OBJECT_FOREIGN_TABLE:
+			{
+				RangeVar *rel = makeRangeVarFromNameList((List *) lfirst(obj));
+				_maybeAddSeparator(&buf, ", ", &first);
+				appendStringInfoString(&buf, RangeVarToString(rel));
+
+				cmd->schemaname = rel->schemaname;
+				cmd->objectname = rel->relname;
+				break;
+			}
+
+			case OBJECT_TYPE:
+			case OBJECT_DOMAIN:
+			{
+				TypeName *typename = makeTypeNameFromNameList((List *) obj);
+				_maybeAddSeparator(&buf, ", ", &first);
+				appendStringInfoString(&buf, TypeNameToString(typename));
+
+				if (list_nth((List *) obj, 1) == NIL)
+				{
+					cmd->schemaname = NULL;
+					cmd->objectname = strVal(linitial((List *) obj));
+				}
+				else
+				{
+					cmd->schemaname = strVal(list_nth((List *) obj, 0));
+					cmd->objectname = strVal(list_nth((List *) obj, 1));
+				}
+				break;
+			}
+
+			/* case OBJECT_COLLATION: */
+			/* case OBJECT_CONVERSION: */
+			/* case OBJECT_SCHEMA: */
+			/* case OBJECT_EXTENSION: */
+			default:
+			{
+				char *name = strVal(linitial((List *) obj));
+				_maybeAddSeparator(&buf, ", ", &first);
+				appendStringInfoString(&buf, name);
+
+				cmd->schemaname = NULL;
+				cmd->objectname = name;
+				break;
+			}
+		}
+	}
+	appendStringInfo(&buf, "%s %s;",
+					 node->missing_ok ? " IF EXISTS":"",
+					 node->behavior == DROP_CASCADE ? "CASCADE" : "RESTRICT");
+
+	cmd->cmdstr = buf.data;
+}
+
+static void
+_rwCreateExtensionStmt(CommandContext cmd, CreateExtensionStmt *node)
+{
+	StringInfoData buf;
+	ListCell   *lc;
+
+	initStringInfo(&buf);
+	appendStringInfo(&buf, "CREATE EXTENSION%s %s",
+					 node->if_not_exists ? " IF NOT EXISTS" : "",
+					 node->extname);
+
+	foreach(lc, node->options)
+	{
+		DefElem    *defel = (DefElem *) lfirst(lc);
+
+		if (strcmp(defel->defname, "schema") == 0)
+			appendStringInfo(&buf, " SCHEMA %s", strVal(defel->arg));
+
+		else if (strcmp(defel->defname, "new_version") == 0)
+			appendStringInfo(&buf, " VERSION %s", strVal(defel->arg));
+
+		else if (strcmp(defel->defname, "old_version") == 0)
+			appendStringInfo(&buf, " FROM %s", strVal(defel->arg));
+	}
+	appendStringInfoChar(&buf, ';');
+
+	cmd->cmdstr = buf.data;
+	cmd->schemaname = NULL;
+	cmd->objectname = node->extname;
+}
+
+static void
+_rwViewStmt(CommandContext cmd, ViewStmt *node)
+{
+	StringInfoData buf;
+	Query	   *viewParse;
+
+	initStringInfo(&buf);
+	viewParse = parse_analyze((Node *) copyObject(node->query),
+							  "(unavailable source text)", NULL, 0);
+
+	appendStringInfo(&buf, "CREATE %s %s AS ",
+					 node->replace? "OR REPLACE VIEW": "VIEW",
+					 RangeVarToString(node->view));
+
+	get_query_def(viewParse, &buf, NIL, NULL, 0, 1);
+	appendStringInfoChar(&buf, ';');
+
+	cmd->cmdstr = buf.data;
+	cmd->schemaname = node->view->schemaname;
+	cmd->objectname = node->view->relname;
+}
+
+static void
+_rwColQualList(StringInfo buf, List *constraints, const char *relname)
+{
+	ListCell   *lc;
+
+	foreach(lc, constraints)
+	{
+		Constraint *c = (Constraint *) lfirst(lc);
+
+		if (c->conname != NULL)
+			appendStringInfo(buf, " CONSTRAINT %s", c->conname);
+
+		switch (c->contype)
+		{
+			case CONSTR_NOTNULL:
+				appendStringInfo(buf, " NOT NULL");
+				break;
+
+			case CONSTR_NULL:
+				appendStringInfo(buf, " NULL");
+				break;
+
+			case CONSTR_UNIQUE:
+				appendStringInfo(buf, " UNIQUE");
+				if (c->indexspace != NULL)
+					appendStringInfo(buf, " USING INDEX TABLESPACE %s", c->indexspace);
+				break;
+
+			case CONSTR_PRIMARY:
+				appendStringInfo(buf, " PRIMARY KEY");
+				if (c->keys != NULL)
+				{
+					ListCell *k;
+					bool first = true;
+
+					appendStringInfoChar(buf, '(');
+					foreach(k, c->keys)
+					{
+						_maybeAddSeparator(buf, ",", &first);
+						appendStringInfo(buf, "%s", strVal(lfirst(k)));
+					}
+					appendStringInfoChar(buf, ')');
+				}
+				if (c->indexspace != NULL)
+					appendStringInfo(buf, " USING INDEX TABLESPACE %s", c->indexspace);
+				break;
+
+			case CONSTR_CHECK:
+			{
+				char	   *consrc;
+				List	   *context;
+
+				context = deparse_context_for(relname, InvalidOid);
+				consrc = deparse_expression(c->raw_expr, context, false, false);
+				appendStringInfo(buf, " CHECK (%s)", consrc);
+				break;
+			}
+
+			case CONSTR_DEFAULT:
+			{
+				char	   *consrc;
+				List	   *context;
+
+				context = deparse_context_for(relname, InvalidOid);
+				consrc = deparse_expression(c->raw_expr, context, false, false);
+				appendStringInfo(buf, " DEFAUT %s", consrc);
+				break;
+			}
+
+			case CONSTR_EXCLUSION:
+				appendStringInfo(buf, " EXCLUDE %s () ", c->access_method);
+				if (c->indexspace != NULL)
+					appendStringInfo(buf, " USING INDEX TABLESPACE %s", c->indexspace);
+				break;
+
+			case CONSTR_FOREIGN:
+				appendStringInfo(buf, " REFERENCES %s()", RangeVarToString(c->pktable));
+				break;
+
+			case CONSTR_ATTR_DEFERRABLE:
+				appendStringInfo(buf, " DEFERRABLE");
+				break;
+
+			case CONSTR_ATTR_NOT_DEFERRABLE:
+				appendStringInfo(buf, " NOT DEFERRABLE");
+				break;
+
+			case CONSTR_ATTR_DEFERRED:
+				appendStringInfo(buf, " INITIALLY DEFERRED");
+				break;
+
+			case CONSTR_ATTR_IMMEDIATE:
+				appendStringInfo(buf, " INITIALLY IMMEDIATE");
+				break;
+		}
+	}
+}
+
+static void
+_rwCreateStmt(CommandContext cmd, CreateStmt *node)
+{
+	ListCell   *lcmd;
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+	appendStringInfo(&buf, "CREATE TABLE %s %s",
+					 RangeVarToString(node->relation),
+					 node->if_not_exists ? " IF NOT EXISTS" : "");
+
+	appendStringInfoChar(&buf, '(');
+
+	foreach(lcmd, node->tableElts)
+	{
+		Node *elmt = (Node *) lfirst(lcmd);
+
+		switch (nodeTag(elmt))
+		{
+			case T_ColumnDef:
+			{
+				ColumnDef  *c = (ColumnDef *) elmt;
+				appendStringInfo(&buf, "%s %s",
+								 c->colname,
+								 TypeNameToString(c->typeName));
+				_rwColQualList(&buf, c->constraints, node->relation->relname);
+				break;
+			}
+			case T_InhRelation:
+			{
+				InhRelation  *r = (InhRelation *) elmt;
+				appendStringInfo(&buf, "%s", RangeVarToString(r->relation));
+				break;
+			}
+			case T_Constraint:
+			{
+				Constraint  *c = (Constraint *) elmt;
+				_rwColQualList(&buf, list_make1(c), node->relation->relname);
+				break;
+			}
+			default:
+				/* Many nodeTags are not interesting as an
+				 * OptTableElementList
+				 */
+				break;
+		}
+		if (lnext(lcmd) != NULL)
+			appendStringInfoChar(&buf, ',');
+	}
+	appendStringInfoChar(&buf, ')');
+	appendStringInfoChar(&buf, ';');
+
+	cmd->cmdstr = buf.data;
+	cmd->schemaname = node->relation->schemaname;
+	cmd->objectname = node->relation->relname;
+}
+
+static void
+_rwAlterTableStmt(CommandContext cmd, AlterTableStmt *node)
+{
+	StringInfoData buf;
+	ListCell   *lcmd;
+	bool        first = true;
+
+	initStringInfo(&buf);
+	appendStringInfo(&buf, "ALTER %s %s",
+					 relkindToString(node->relkind),
+					 RangeVarToString(node->relation));
+
+	foreach(lcmd, node->cmds)
+	{
+		AlterTableCmd *cmd = (AlterTableCmd *) lfirst(lcmd);
+		ColumnDef  *def = (ColumnDef *) cmd->def;
+
+		_maybeAddSeparator(&buf, ", ", &first);
+
+		switch (cmd->subtype)
+		{
+			case AT_AddColumn:				/* add column */
+				appendStringInfo(&buf, " ADD COLUMN %s %s",
+								 def->colname,
+								 TypeNameToString(def->typeName));
+
+				if (def->is_not_null)
+					appendStringInfoString(&buf, " NOT NULL");
+				break;
+
+			case AT_ColumnDefault:			/* alter column default */
+				if (def == NULL)
+					appendStringInfo(&buf, " ALTER %s DROP DEFAULT",
+									 cmd->name);
+				else
+				{
+					char *str =
+						deparse_expression_pretty(cmd->def, NIL, false, false, 0, 0);
+
+					appendStringInfo(&buf, " ALTER %s SET DEFAULT %s",
+									 cmd->name, str);
+				}
+				break;
+
+			case AT_DropNotNull:			/* alter column drop not null */
+				appendStringInfo(&buf, " ALTER %s DROP NOT NULL", cmd->name);
+				break;
+
+			case AT_SetNotNull:				/* alter column set not null */
+				appendStringInfo(&buf, " ALTER %s SET NOT NULL", cmd->name);
+				break;
+
+			case AT_SetStatistics:			/* alter column set statistics */
+				appendStringInfo(&buf, " ALTER %s SET STATISTICS %ld",
+								 cmd->name,
+								 (long) intVal((Value *)(cmd->def)));
+				break;
+
+			case AT_SetOptions:				/* alter column set ( options ) */
+				break;
+
+			case AT_ResetOptions:			/* alter column reset ( options ) */
+				break;
+
+			case AT_SetStorage:				/* alter column set storage */
+				appendStringInfo(&buf, " ALTER %s SET STORAGE %s",
+								 cmd->name,
+								 strVal((Value *)(cmd->def)));
+				break;
+
+			case AT_DropColumn:				/* drop column */
+				appendStringInfo(&buf, " %s %s%s",
+								 cmd->missing_ok? "DROP IF EXISTS": "DROP",
+								 cmd->name,
+								 cmd->behavior == DROP_CASCADE? " CASCADE": "");
+				break;
+
+			case AT_AddIndex:				/* add index */
+				break;
+
+			case AT_AddConstraint:			/* add constraint */
+				break;
+
+			case AT_ValidateConstraint:		/* validate constraint */
+				appendStringInfo(&buf, " VALIDATE CONSTRAINT %s", cmd->name);
+				break;
+
+			case AT_AddIndexConstraint:		/* add constraint using existing index */
+				break;
+
+			case AT_DropConstraint:			/* drop constraint */
+				appendStringInfo(&buf, " DROP CONSTRAINT%s %s %s",
+								 cmd->missing_ok? " IF EXISTS": "",
+								 cmd->name,
+								 cmd->behavior == DROP_CASCADE? " CASCADE": "");
+				break;
+
+			case AT_AlterColumnType:		/* alter column type */
+				appendStringInfo(&buf, " ALTER %s TYPE %s",
+								 cmd->name,
+								 TypeNameToString(def->typeName));
+				if (def->raw_default != NULL)
+				{
+					char *str =
+						deparse_expression_pretty(def->raw_default,
+												  NIL, false, false, 0, 0);
+					appendStringInfo(&buf, " USING %s", str);
+				}
+				break;
+
+			case AT_AlterColumnGenericOptions:	/* alter column OPTIONS (...) */
+				break;
+
+			case AT_ChangeOwner:			/* change owner */
+				appendStringInfo(&buf, " OWNER TO %s", cmd->name);
+				break;
+
+			case AT_ClusterOn:				/* CLUSTER ON */
+				appendStringInfo(&buf, " CLUSTER ON %s", cmd->name);
+				break;
+
+			case AT_DropCluster:			/* SET WITHOUT CLUSTER */
+				appendStringInfo(&buf, " SET WITHOUT CLUSTER");
+				break;
+
+			case AT_SetTableSpace:			/* SET TABLESPACE */
+				appendStringInfo(&buf, " SET TABLESPACE %s", cmd->name);
+				break;
+
+			case AT_SetRelOptions:			/* SET (...) -- AM specific parameters */
+				break;
+
+			case AT_ResetRelOptions:		/* RESET (...) -- AM specific parameters */
+				break;
+
+			case AT_EnableTrig:				/* ENABLE TRIGGER name */
+				appendStringInfo(&buf, " ENABLE TRIGGER %s", cmd->name);
+				break;
+
+			case AT_EnableAlwaysTrig:		/* ENABLE ALWAYS TRIGGER name */
+				appendStringInfo(&buf, " ENABLE ALWAYS TRIGGER %s", cmd->name);
+				break;
+
+			case AT_EnableReplicaTrig:		/* ENABLE REPLICA TRIGGER name */
+				appendStringInfo(&buf, " ENABLE REPLICA TRIGGER %s", cmd->name);
+				break;
+
+			case AT_DisableTrig:			/* DISABLE TRIGGER name */
+				appendStringInfo(&buf, " DISABLE TRIGGER %s", cmd->name);
+				break;
+
+			case AT_EnableTrigAll:			/* ENABLE TRIGGER ALL */
+				appendStringInfo(&buf, " ENABLE TRIGGER ALL");
+				break;
+
+			case AT_DisableTrigAll:			/* DISABLE TRIGGER ALL */
+				appendStringInfo(&buf, " DISABLE TRIGGER ALL");
+				break;
+
+			case AT_EnableTrigUser:			/* ENABLE TRIGGER USER */
+				appendStringInfo(&buf, " ENABLE TRIGGER USER");
+				break;
+
+			case AT_DisableTrigUser:		/* DISABLE TRIGGER USER */
+				appendStringInfo(&buf, " DISABLE TRIGGER USER");
+				break;
+
+			case AT_EnableRule:				/* ENABLE RULE name */
+				appendStringInfo(&buf, " ENABLE RULE %s", cmd->name);
+				break;
+
+			case AT_EnableAlwaysRule:		/* ENABLE ALWAYS RULE name */
+				appendStringInfo(&buf, " ENABLE ALWAYS RULE %s", cmd->name);
+				break;
+
+			case AT_EnableReplicaRule:		/* ENABLE REPLICA RULE name */
+				appendStringInfo(&buf, " ENABLE REPLICA RULE %s", cmd->name);
+				break;
+
+			case AT_DisableRule:			/* DISABLE RULE name */
+				appendStringInfo(&buf, " DISABLE RULE %s", cmd->name);
+				break;
+
+			case AT_AddInherit:				/* INHERIT parent */
+				appendStringInfo(&buf, " INHERIT %s",
+								 RangeVarToString((RangeVar *) cmd->def));
+				break;
+
+			case AT_DropInherit:			/* NO INHERIT parent */
+				appendStringInfo(&buf, " NO INHERIT %s",
+								 RangeVarToString((RangeVar *) cmd->def));
+				break;
+
+			case AT_AddOf:					/* OF <type_name> */
+				appendStringInfo(&buf, " OF %s", TypeNameToString(def->typeName));
+				break;
+
+			case AT_DropOf:					/* NOT OF */
+				appendStringInfo(&buf, " NOT OF");
+				break;
+
+			case AT_GenericOptions:			/* OPTIONS (...) */
+				break;
+
+			default:
+				break;
+		}
+	}
+	appendStringInfoChar(&buf, ';');
+
+	cmd->cmdstr = buf.data;
+	cmd->schemaname = node->relation->schemaname;
+	cmd->objectname = node->relation->relname;
+}
+
+/*
+ * get_cmddef
+ *
+ * internal use only, used for DDL Triggers
+ *
+ * Utility statements are not planned thus won't get into a Query *, we get to
+ * work from the parsetree directly, that would be query->utilityStmt which is
+ * of type Node *. We declare that a void * to avoid incompatible pointer type
+ * warnings.
+ */
+void
+pg_get_cmddef(CommandContext cmd, void *parsetree)
+{
+	cmd->nodestr = nodeToString(parsetree);
+	/* elog(NOTICE, "nodeToString: %s", cmd->nodestr); */
+	stringToNode(cmd->nodestr);
+
+	/*
+	 * we need the big'o'switch here, and calling a specialized function per
+	 * utility statement nodetag.
+	 */
+
+	switch (nodeTag(parsetree))
+	{
+		case T_DropStmt:
+			_rwDropStmt(cmd, parsetree);
+			break;
+
+		case T_CreateStmt:
+			_rwCreateStmt(cmd, parsetree);
+			break;
+
+		case T_AlterTableStmt:
+			_rwAlterTableStmt(cmd, parsetree);
+			break;
+
+		case T_ViewStmt:
+			_rwViewStmt(cmd, parsetree);
+			break;
+
+		case T_CreateExtensionStmt:
+			_rwCreateExtensionStmt(cmd, parsetree);
+			break;
+
+		default:
+			/* is it best to elog(ERROR)?  Not while in development :) */
+			elog(DEBUG2, "unrecognized node type: %d",
+				 (int) nodeTag(parsetree));
+	}
+}
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 5302e50..a7220a0 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -146,6 +146,7 @@ typedef enum ObjectClass
 	OCLASS_USER_MAPPING,		/* pg_user_mapping */
 	OCLASS_DEFACL,				/* pg_default_acl */
 	OCLASS_EXTENSION,			/* pg_extension */
+	OCLASS_CMDTRIGGER,			/* pg_cmdtrigger */
 	MAX_OCLASS					/* MUST BE LAST */
 } ObjectClass;
 
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 2e5d224..cc5b454 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -234,6 +234,11 @@ DECLARE_UNIQUE_INDEX(pg_trigger_tgrelid_tgname_index, 2701, on pg_trigger using
 DECLARE_UNIQUE_INDEX(pg_trigger_oid_index, 2702, on pg_trigger using btree(oid oid_ops));
 #define TriggerOidIndexId  2702
 
+DECLARE_UNIQUE_INDEX(pg_cmdtrigger_ctgcommand_ctgname_index, 3467, on pg_cmdtrigger using btree(ctgcommand name_ops, ctgname name_ops));
+#define CmdTriggerCommandNameIndexId  3467
+DECLARE_UNIQUE_INDEX(pg_cmdtrigger_oid_index, 3468, on pg_cmdtrigger using btree(oid oid_ops));
+#define CmdTriggerOidIndexId  3468
+
 DECLARE_UNIQUE_INDEX(pg_ts_config_cfgname_index, 3608, on pg_ts_config using btree(cfgname name_ops, cfgnamespace oid_ops));
 #define TSConfigNameNspIndexId	3608
 DECLARE_UNIQUE_INDEX(pg_ts_config_oid_index, 3712, on pg_ts_config using btree(oid oid_ops));
diff --git a/src/include/catalog/pg_cmdtrigger.h b/src/include/catalog/pg_cmdtrigger.h
new file mode 100644
index 0000000..dc12dad
--- /dev/null
+++ b/src/include/catalog/pg_cmdtrigger.h
@@ -0,0 +1,59 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_cmdtrigger.h
+ *	  definition of the system "command trigger" relation (pg_cmdtrigger)
+ *	  along with the relation's initial contents.
+ *
+ *
+ * Portions Copyright (c) 1996-2011, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_trigger.h
+ *
+ * NOTES
+ *	  the genbki.pl script reads this file and generates .bki
+ *	  information from the DATA() statements.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_CMDTRIGGER_H
+#define PG_CMDTRIGGER_H
+
+#include "catalog/genbki.h"
+
+/* ----------------
+ *		pg_cmdtrigger definition.	cpp turns this into
+ *		typedef struct FormData_pg_cmdtrigger
+ * ----------------
+ */
+#define CmdTriggerRelationId  3466
+
+CATALOG(pg_cmdtrigger,3466)
+{
+	NameData    ctgcommand;		/* trigger's command */
+	NameData	ctgname;		/* trigger's name */
+	Oid			ctgfoid;		/* OID of function to be called */
+	char		ctgtype;		/* BEFORE/AFTER/INSTEAD */
+	char		ctgenabled;		/* trigger's firing configuration WRT
+								 * session_replication_role */
+} FormData_pg_cmdtrigger;
+
+/* ----------------
+ *		Form_pg_cmdtrigger corresponds to a pointer to a tuple with
+ *		the format of pg_cmdtrigger relation.
+ * ----------------
+ */
+typedef FormData_pg_cmdtrigger *Form_pg_cmdtrigger;
+
+/* ----------------
+ *		compiler constants for pg_cmdtrigger
+ * ----------------
+ */
+#define Natts_pg_cmdtrigger					5
+#define Anum_pg_cmdtrigger_ctgcommand		1
+#define Anum_pg_cmdtrigger_ctgname			2
+#define Anum_pg_cmdtrigger_ctgfoid			3
+#define Anum_pg_cmdtrigger_ctgtype			4
+#define Anum_pg_cmdtrigger_ctgenabled		5
+
+#endif   /* PG_CMDTRIGGER_H */
diff --git a/src/include/commands/cmdtrigger.h b/src/include/commands/cmdtrigger.h
new file mode 100644
index 0000000..f7d0980
--- /dev/null
+++ b/src/include/commands/cmdtrigger.h
@@ -0,0 +1,45 @@
+/*-------------------------------------------------------------------------
+ *
+ * cmdtrigger.h
+ *	  Declarations for command trigger handling.
+ *
+ * Portions Copyright (c) 1996-2011, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/cmdtrigger.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef CMDTRIGGER_H
+#define CMDTRIGGER_H
+
+#include "nodes/execnodes.h"
+#include "nodes/parsenodes.h"
+
+/*
+ * Times at which a command trigger can be fired. These are the
+ * possible values for pg_cmdtrigger.ctgtype.
+ *
+ * pg_trigger is using binary mask tricks to make it super fast, but we don't
+ * need to be that tricky here: we're talking about commands, not data editing,
+ * and we don't have so many conditions, only type and enabled.
+ */
+#define CMD_TRIGGER_FIRED_BEFORE			'B'
+#define CMD_TRIGGER_FIRED_AFTER				'A'
+#define CMD_TRIGGER_FIRED_INSTEAD			'I'
+
+extern Oid CreateCmdTrigger(CreateCmdTrigStmt *stmt, const char *queryString);
+extern void DropCmdTrigger(DropCmdTrigStmt *stmt);
+extern void RemoveCmdTriggerById(Oid ctrigOid);
+extern Oid	get_cmdtrigger_oid(const char *command, const char *trigname, bool missing_ok);
+extern void AlterCmdTrigger(AlterCmdTrigStmt *stmt);
+extern void RenameCmdTrigger(List *command, const char *trigname, const char *newname);
+
+int ExecBeforeOrInsteadOfCommandTriggers(Node *parsetree, const char *command);
+int ExecInsteadOfCommandTriggers(Node *parsetree, const char *command,
+								 MemoryContext per_command_context);
+bool ExecBeforeCommandTriggers(Node *parsetree, const char *command,
+							   MemoryContext per_command_context);
+void ExecAfterCommandTriggers(Node *parsetree, const char *command);
+
+#endif   /* TRIGGER_H */
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index 70d3a8f..e158fc5 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -175,4 +175,18 @@ extern TypeName *defGetTypeName(DefElem *def);
 extern int	defGetTypeLength(DefElem *def);
 extern DefElem *defWithOids(bool value);
 
+/* utils/adt/ruleutils.c -- FIXME, find a better place */
+typedef struct CommandContextData
+{
+	char *tag;					/* Command Tag */
+	char *cmdstr;				/* Command String, rewritten by ruleutils */
+	char *nodestr;				/* nodeToString(parsetree) */
+	char *schemaname;			/* schemaname or NULL if not relevant */
+	char *objectname;			/* objectname */
+} CommandContextData;
+
+typedef struct CommandContextData *CommandContext;
+
+extern void pg_get_cmddef(CommandContext cmd, void *parsetree);
+
 #endif   /* DEFREM_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 3a24089..e1336bd 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -291,6 +291,7 @@ typedef enum NodeTag
 	T_IndexStmt,
 	T_CreateFunctionStmt,
 	T_AlterFunctionStmt,
+	T_RemoveFuncStmt,
 	T_DoStmt,
 	T_RenameStmt,
 	T_RuleStmt,
@@ -311,7 +312,9 @@ typedef enum NodeTag
 	T_VariableShowStmt,
 	T_DiscardStmt,
 	T_CreateTrigStmt,
+	T_DropPropertyStmt,
 	T_CreatePLangStmt,
+	T_DropPLangStmt,
 	T_CreateRoleStmt,
 	T_AlterRoleStmt,
 	T_DropRoleStmt,
@@ -325,9 +328,12 @@ typedef enum NodeTag
 	T_AlterRoleSetStmt,
 	T_CreateConversionStmt,
 	T_CreateCastStmt,
+	T_DropCastStmt,
 	T_CreateOpClassStmt,
 	T_CreateOpFamilyStmt,
 	T_AlterOpFamilyStmt,
+	T_RemoveOpClassStmt,
+	T_RemoveOpFamilyStmt,
 	T_PrepareStmt,
 	T_ExecuteStmt,
 	T_DeallocateStmt,
@@ -346,8 +352,10 @@ typedef enum NodeTag
 	T_AlterTSConfigurationStmt,
 	T_CreateFdwStmt,
 	T_AlterFdwStmt,
+	T_DropFdwStmt,
 	T_CreateForeignServerStmt,
 	T_AlterForeignServerStmt,
+	T_DropForeignServerStmt,
 	T_CreateUserMappingStmt,
 	T_AlterUserMappingStmt,
 	T_DropUserMappingStmt,
@@ -357,6 +365,9 @@ typedef enum NodeTag
 	T_CreateExtensionStmt,
 	T_AlterExtensionStmt,
 	T_AlterExtensionContentsStmt,
+	T_CreateCmdTrigStmt,
+	T_DropCmdTrigStmt,
+	T_AlterCmdTrigStmt,
 
 	/*
 	 * TAGS FOR PARSE TREE NODES (parsenodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9e277c5..4469314 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1106,6 +1106,7 @@ typedef enum ObjectType
 	OBJECT_AGGREGATE,
 	OBJECT_ATTRIBUTE,			/* type's attribute, when distinct from column */
 	OBJECT_CAST,
+	OBJECT_CMDTRIGGER,
 	OBJECT_COLUMN,
 	OBJECT_CONSTRAINT,
 	OBJECT_COLLATION,
@@ -1723,6 +1724,34 @@ typedef struct CreateTrigStmt
 } CreateTrigStmt;
 
 /* ----------------------
+ *		Create COMMAND TRIGGER Statement
+ * ----------------------
+ */
+typedef struct CreateCmdTrigStmt
+{
+	NodeTag		type;
+	char	   *command;		/* command's name */
+	char	   *trigname;			/* TRIGGER's name */
+	/* timing uses the TRIGGER_TYPE bits defined in catalog/pg_trigger.h */
+	int16		timing;			/* BEFORE, AFTER, or INSTEAD */
+	List	   *funcname;		/* qual. name of function to call */
+} CreateCmdTrigStmt;
+
+/* ----------------------
+ *		Alter COMMAND TRIGGER Statement
+ * ----------------------
+ */
+typedef struct AlterCmdTrigStmt
+{
+	NodeTag		type;
+	char	   *command;		/* command's name */
+	char	   *trigname;		/* TRIGGER's name */
+	char       *tgenabled;		/* trigger's firing configuration WRT
+								 * session_replication_role */
+} AlterCmdTrigStmt;
+
+/* ----------------------
+ *		Create/Drop PROCEDURAL LANGUAGE Statements
  *		Create PROCEDURAL LANGUAGE Statements
  * ----------------------
  */
@@ -1904,6 +1933,37 @@ typedef struct DropStmt
 } DropStmt;
 
 /* ----------------------
+ *		Drop Rule|Trigger Statement
+ *
+ * In general this may be used for dropping any property of a relation;
+ * for example, someday soon we may have DROP ATTRIBUTE.
+ * ----------------------
+ */
+
+typedef struct DropPropertyStmt
+{
+	NodeTag		type;
+	RangeVar   *relation;		/* owning relation */
+	char	   *property;		/* name of rule, trigger, etc */
+	ObjectType	removeType;		/* OBJECT_RULE or OBJECT_TRIGGER */
+	DropBehavior behavior;		/* RESTRICT or CASCADE behavior */
+	bool		missing_ok;		/* skip error if missing? */
+} DropPropertyStmt;
+
+/* ----------------------
+ *		Drop Command Trigger Statement
+ * ----------------------
+ */
+typedef struct DropCmdTrigStmt
+{
+	NodeTag		type;
+	char	   *command;		/* command's name */
+	char	   *trigname;		/* TRIGGER's name */
+	DropBehavior behavior;		/* RESTRICT or CASCADE behavior */
+	bool		missing_ok;		/* skip error if missing? */
+} DropCmdTrigStmt;
+
+/* ----------------------
  *				Truncate Table Statement
  * ----------------------
  */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 3d170bc..bb949d0 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -81,6 +81,7 @@ PG_KEYWORD("coalesce", COALESCE, COL_NAME_KEYWORD)
 PG_KEYWORD("collate", COLLATE, RESERVED_KEYWORD)
 PG_KEYWORD("collation", COLLATION, UNRESERVED_KEYWORD)
 PG_KEYWORD("column", COLUMN, RESERVED_KEYWORD)
+PG_KEYWORD("command", COMMAND, UNRESERVED_KEYWORD)
 PG_KEYWORD("comment", COMMENT, UNRESERVED_KEYWORD)
 PG_KEYWORD("comments", COMMENTS, UNRESERVED_KEYWORD)
 PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD)
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index cb468e5..b19fcbe 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -92,6 +92,7 @@ SELECT relname, relhasindex
  pg_authid               | t
  pg_cast                 | t
  pg_class                | t
+ pg_cmdtrigger           | t
  pg_collation            | t
  pg_constraint           | t
  pg_conversion           | t
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index b4d3919..3892d36 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -1443,3 +1443,38 @@ NOTICE:  drop cascades to 2 other objects
 DETAIL:  drop cascades to view city_view
 drop cascades to view european_city_view
 DROP TABLE country_table;
+CREATE FUNCTION cmdtrigger_notice
+ (
+   IN cmd_string     text,
+   IN cmd_nodestring text,
+   IN schemaname     text,
+   IN relname        text
+ )
+ RETURNS void
+ LANGUAGE plpgsql
+AS $$
+BEGIN
+  RAISE NOTICE 'cmd_string: %', cmd_string;
+END;
+$$;
+CREATE TRIGGER cmdtrigger_notice
+        AFTER COMMAND CREATE TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+CREATE TRIGGER cmdtrigger_notice
+        AFTER COMMAND DROP TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+-- that should error out as you can't have both INSTEAD OF command triggers
+-- and BEFORE|AFTER triggers defined on the same command
+CREATE TRIGGER cmdtrigger_notice_error
+    INSTEAD OF COMMAND DROP TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+ERROR:  "DROP TABLE" already has AFTER triggers
+DETAIL:  Commands cannot have both AFTER and INSTEAD OF triggers.
+CREATE TABLE foo(a serial, b text, primary key (a, b));
+NOTICE:  CREATE TABLE will create implicit sequence "foo_a_seq" for serial column "foo.a"
+NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"
+NOTICE:  cmd_string: CREATE TABLE foo (a serial,b text, PRIMARY KEY(a,b));
+DROP TABLE foo;
+NOTICE:  cmd_string: DROP TABLE foo RESTRICT;
+DROP TRIGGER cmdtrigger_notice ON COMMAND CREATE TABLE;
+DROP TRIGGER cmdtrigger_notice ON COMMAND DROP TABLE;
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index e52131d..f944d60 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -961,3 +961,38 @@ SELECT * FROM city_view;
 
 DROP TABLE city_table CASCADE;
 DROP TABLE country_table;
+
+CREATE FUNCTION cmdtrigger_notice
+ (
+   IN cmd_string     text,
+   IN cmd_nodestring text,
+   IN schemaname     text,
+   IN relname        text
+ )
+ RETURNS void
+ LANGUAGE plpgsql
+AS $$
+BEGIN
+  RAISE NOTICE 'cmd_string: %', cmd_string;
+END;
+$$;
+
+CREATE TRIGGER cmdtrigger_notice
+        AFTER COMMAND CREATE TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+
+CREATE TRIGGER cmdtrigger_notice
+        AFTER COMMAND DROP TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+
+-- that should error out as you can't have both INSTEAD OF command triggers
+-- and BEFORE|AFTER triggers defined on the same command
+CREATE TRIGGER cmdtrigger_notice_error
+    INSTEAD OF COMMAND DROP TABLE
+       EXECUTE PROCEDURE cmdtrigger_notice();
+
+CREATE TABLE foo(a serial, b text, primary key (a, b));
+DROP TABLE foo;
+
+DROP TRIGGER cmdtrigger_notice ON COMMAND CREATE TABLE;
+DROP TRIGGER cmdtrigger_notice ON COMMAND DROP TABLE;
#3Andres Freund
andres@anarazel.de
In reply to: Dimitri Fontaine (#1)
3 attachment(s)
Re: Command Triggers

Hi Dimitri, Hi all,

On Tuesday, November 08, 2011 06:47:13 PM Dimitri Fontaine wrote:

As proposed by Jan Wieck on hackers and discussed in Ottawa at the
Clusters Hackers Meeting, the most (only?) workable way to ever have DDL
triggers is to have "command triggers" instead.
Rather than talking about catalogs and the like, it's all about firing a
registered user-defined function before or after processing the utility
command, or instead of processing it. This naturally involves a new
catalog (pg_cmdtrigger) and some new subcommands of CREATE and DROP
TRIGGER, and some support code for calling the function at the right
time.

Great ;)

fwiw I think thats the way forward as well.

The patch obviously isn't thought to be commitable yet, so I am not going to
do a very detailed code level review. Attached is a patch with low level
targeted comments and such I noticed while reading it.

At this state the important things are highlevel so I will try to concentrate
on those:

== the trigger function ==
I don't like the set of options parsed to the trigger functions very much. To
cite an example of yours those currently are:
* cmd_string text
* cmd_nodestring text
* schemaname text
* relname text

I think at least a
* command_tag text
is missing. Sure, you can disambiguate it by creating a function for every
command type but that seems pointlessly complex for many applications. And
adding it should be trivial as its already tracked ;)

Currently the examples refer to relname as relname which I dislike as that
seems to be too restricted to one use case. The code is already doing it
correctly (as objectname) though ;)

Why is there explicit documentation of not checking the arguments of said
trigger function? That seems to be a bit strange to me.

schemaname currently is mightily unusable because whether its sent or not
depends wether the table was created with a schemaname specified or not. That
doesn't seem to be a good approach.

That brings me to the next point:

== normalization of statements ==
Currently the normalization is a bit confusing. E.g. for:

CREATE SCHEMA barf;
SET search_path = barf;
CREATE TYPE bart AS ENUM ('a', 'b');
CREATE TABLE bar(a int, b bigint, c serial, d text, e varchar, "F" text, g
bart, h int4);

one gets

CREATE TABLE bar (a pg_catalog.int4,b pg_catalog.int8,c serial,d text,e
pg_catalog.varchar,F text,g bart,h int4);

Which points out two problems:
* inconsistent schema prefixing
* no quoting

Imo the output should fully schema qualify anything including operators. Yes,
thats rather ugly but anything else seems to be too likely to lead to
problems.

As an alternative it would be possible to pass the current search path but
that doesn't seem to the best approach to me;

Currently CHECK() constraints are not decodable, but inside those the same
quoting/qualifying rules should apply.

Then there is nice stuff like CREATE TABLE .... (LIKE ...);
What shall that return in future? I vote for showing it as the plain CREATE
TABLE where all columns are specified.

That touches another related topic. Dim's work in adding support for utility
cmd support in nodeToString and friends possibly will make the code somewhat
command trigger specific. Is there any other usage we envision?

== grammar ==

* multiple actions:
I think it would sensible to allow multiple actions on which to trigger to be
specified just as you can for normal triggers. I also would like an ALL option

* options:
Currently the grammar allows options to be passed to command triggers. Do we
want to keep that? If yes, with the same arcane set of datatypes as in data
triggers?
If yes it needs to be wired up.

* schema qualification:
An option to add triggers only to a specific schema would be rather neat for
many scenarios.
I am not totally sure if its worth the hassle of defining what happens in the
edge cases of e.g. moving from one into another schema though.

== locking ==

Currently there is no locking strategy at all. Which e.g. means that there is
no protection against two sessions changing stuff concurrently or such.

Was that just left out - which is ok - or did you miss that?

I think it would be ok to just always grab row level locks there.

== permissions ==

Command triggers should only be allowed for the database owner.

== recursion ==
I contrast to data triggers command triggers cannot change what is done. They
can either prevent it from running or not. Which imo is fine.
The question I have is whether it would be a good idea to make it easy to
prevent recursion.... Especially if the triggers wont be per schema.

== calling the trigger ==

Imo the current callsite in ProcessUtility isn't that good. I think it would
make more sense moving it into standard_ProcessUtility. It should be *after*
the check_xact_readonly check in there in my opinion.

I am also don't think its a good idea to run the
ExecBeforeOrInsteadOfCommandTriggers stuff there because thats allso the path
that COMMIT/BEGIN and other stuff take. Those are pretty "fast path".

I suggest making two switches in standard_ProcessUtility, one for the non-
command trigger stuff which returns on success and one after that. Only after
the first triggers are checked.

I wonder whether its the right choice to run triggers on untransformed input.
I.e. CREATE TABLE ... (LIKE ...) seems to only make sense to me after
transforming.

Also youre very repeatedly transforming the parstree into a string. It would
be better doing that only once instead of every trigger...

Ok, thats the first round of high level stuff...

Cool Work!

Some lower level annotations:

* many functions should be static but are not. Especially in cmdtrigger.c
* Either tgenable's datatype or its readfunc is wrong (watch the compiler ;))
* ruleutils.c:
* generic routine for IF EXISTS, CASCADE, ...

Greetings,

Andres

Attachments:

0001-Fix-dependency-recording-for-command-triggers.patchtext/x-patch; charset=UTF-8; name=0001-Fix-dependency-recording-for-command-triggers.patchDownload
From 99a67fa63e5bcec6631c00253653717688bbdfde Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Thu, 1 Dec 2011 14:18:55 +0100
Subject: [PATCH 1/3] Fix dependency recording for command triggers

* the oid is only defined after heap_insert
* use the correct relationship as classid
---
 src/backend/commands/cmdtrigger.c |   11 ++++-------
 1 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/src/backend/commands/cmdtrigger.c b/src/backend/commands/cmdtrigger.c
index 5a4d370..c6cda29 100644
--- a/src/backend/commands/cmdtrigger.c
+++ b/src/backend/commands/cmdtrigger.c
@@ -157,16 +157,13 @@ CreateCmdTrigger(CreateCmdTrigStmt *stmt, const char *queryString)
 
 	tuple = heap_form_tuple(tgrel->rd_att, values, nulls);
 
-	/* force tuple to have the desired OID */
-	trigoid = HeapTupleGetOid(tuple);
-
-	/*
-	 * Insert tuple into pg_trigger.
-	 */
 	simple_heap_insert(tgrel, tuple);
 
 	CatalogUpdateIndexes(tgrel, tuple);
 
+	/* remember oid for record dependencies */
+	trigoid = HeapTupleGetOid(tuple);
+
 	heap_freetuple(tuple);
 	heap_close(tgrel, RowExclusiveLock);
 
@@ -174,7 +171,7 @@ CreateCmdTrigger(CreateCmdTrigStmt *stmt, const char *queryString)
 	 * Record dependencies for trigger.  Always place a normal dependency on
 	 * the function.
 	 */
-	myself.classId = TriggerRelationId;
+	myself.classId = CmdTriggerRelationId;
 	myself.objectId = trigoid;
 	myself.objectSubId = 0;
 
-- 
1.7.6.409.ge7a85.dirty

0002-Fix-file-headers-of-new-cmdtrigger.c-file.patchtext/x-patch; charset=UTF-8; name=0002-Fix-file-headers-of-new-cmdtrigger.c-file.patchDownload
From f48b887b5090823a621ff896b01412b044f02f64 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Thu, 1 Dec 2011 14:20:17 +0100
Subject: [PATCH 2/3] Fix file headers of new cmdtrigger.c file

---
 src/backend/commands/cmdtrigger.c |    9 ++++-----
 1 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/backend/commands/cmdtrigger.c b/src/backend/commands/cmdtrigger.c
index c6cda29..733c798 100644
--- a/src/backend/commands/cmdtrigger.c
+++ b/src/backend/commands/cmdtrigger.c
@@ -1,13 +1,12 @@
 /*-------------------------------------------------------------------------
  *
- * trigger.c
- *	  PostgreSQL TRIGGERs support code.
+ * cmdtrigger.c
+ *	  PostgreSQL COMMAND TRIGGER support code.
  *
- * Portions Copyright (c) 1996-2011, PostgreSQL Global Development Group
- * Portions Copyright (c) 1994, Regents of the University of California
+ * Portions Copyright (c) 2011, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
- *	  src/backend/commands/trigger.c
+ *	  src/backend/commands/cmdtrigger.c
  *
  *-------------------------------------------------------------------------
  */
-- 
1.7.6.409.ge7a85.dirty

0003-Add-some-review-comments.patchtext/x-patch; charset=UTF-8; name=0003-Add-some-review-comments.patchDownload
From 016aedc955ccd4cb2eb6cdead6957bf7cb58cee8 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Thu, 1 Dec 2011 17:30:48 +0100
Subject: [PATCH 3/3] Add some review comments

---
 src/backend/catalog/dependency.c  |    2 +-
 src/backend/commands/cmdtrigger.c |   25 +++++++++++++++++++------
 src/backend/nodes/readfuncs.c     |    2 +-
 src/backend/utils/adt/ruleutils.c |   11 +++++++++--
 4 files changed, 30 insertions(+), 10 deletions(-)

diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 42701f3..5e53e08 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -2843,7 +2843,7 @@ getObjectDescription(const ObjectAddress *object)
 
 				trig = (Form_pg_cmdtrigger) GETSTRUCT(tup);
 
-				appendStringInfo(&buffer, _("trigger %s on %s"),
+				appendStringInfo(&buffer, _("command trigger %s on %s"),
 								 NameStr(trig->ctgname),
 								 NameStr(trig->ctgcommand));
 
diff --git a/src/backend/commands/cmdtrigger.c b/src/backend/commands/cmdtrigger.c
index 733c798..3780521 100644
--- a/src/backend/commands/cmdtrigger.c
+++ b/src/backend/commands/cmdtrigger.c
@@ -129,6 +129,8 @@ CreateCmdTrigger(CreateCmdTrigStmt *stmt, const char *queryString)
 						 errdetail("Commands cannot have both AFTER and INSTEAD OF triggers.")));
             break;
 		}
+		default:
+			elog(ERROR, "unknown trigger type for COMMAND TRIGGER");
 	}
 
 	if (ctgtype == CMD_TRIGGER_FIRED_BEFORE && funcrettype != BOOLOID)
@@ -143,6 +145,8 @@ CreateCmdTrigger(CreateCmdTrigStmt *stmt, const char *queryString)
 				 errmsg("function \"%s\" must return type \"void\"",
 						NameListToString(stmt->funcname))));
 
+	//FIXME: check argument types?
+
 	/*
 	 * Build the new pg_trigger tuple.
 	 */
@@ -259,8 +263,8 @@ AlterCmdTrigger(AlterCmdTrigStmt *stmt)
 	Form_pg_cmdtrigger cmdForm;
 	char        tgenabled = pstrdup(stmt->tgenabled)[0]; /* works with gram.y */
 
-	tgrel = heap_open(CmdTriggerRelationId, AccessShareLock);
-
+	tgrel = heap_open(CmdTriggerRelationId, AccessShareLock);//FIXME: wrong lock level?
+	//FIXME: need a row level lock here
 	ScanKeyInit(&skey[0],
 				Anum_pg_cmdtrigger_ctgcommand,
 				BTEqualStrategyNumber, F_NAMEEQ,
@@ -315,6 +319,7 @@ RenameCmdTrigger(List *name, const char *trigname, const char *newname)
 
 	rel = heap_open(CmdTriggerRelationId, RowExclusiveLock);
 
+	//FIXME: need a row level lock here
 	/* newname must be available */
 	check_cmdtrigger_name(command, newname, rel);
 
@@ -432,7 +437,7 @@ check_cmdtrigger_name(const char *command, const char *trigname, Relation tgrel)
 
 	tuple = systable_getnext(tgscan);
 
-	elog(NOTICE, "check_cmdtrigger_name(%s, %s)", command, trigname);
+	elog(DEBUG1, "check_cmdtrigger_name(%s, %s)", command, trigname);
 
 	if (HeapTupleIsValid(tuple))
 		ereport(ERROR,
@@ -464,7 +469,8 @@ check_cmdtrigger_name(const char *command, const char *trigname, Relation tgrel)
  * nodeToString() output.
  *
  */
-
+//FIXME: Return a List here.
+//FIXME: add abort-after-first for CreateCmdTrigger?
 static RegProcedure *
 list_triggers_for_command(const char *command, char type)
 {
@@ -500,7 +506,7 @@ list_triggers_for_command(const char *command, char type)
 		{
 			if (count == size)
 			{
-				size += 10;
+				size += 10;//FIXME: WTF?
 				procs = (Oid *)repalloc(procs, size);
 			}
 			procs[count++] = cmd->ctgfoid;
@@ -560,7 +566,7 @@ call_cmdtrigger_procedure(RegProcedure proc, CommandContext cmd,
  * Instead Of triggers, not both.
  *
  * Instead Of triggers have to run before the command and to cancel its
- * execution , hence this API where we return the number of InsteadOf trigger
+ * execution, hence this API where we return the number of InsteadOf trigger
  * procedures we fired.
  */
 int
@@ -591,6 +597,8 @@ ExecBeforeOrInsteadOfCommandTriggers(Node *parsetree, const char *cmdtag)
 	return nb;
 }
 
+//FIXME: This looks like it should be static
+//FIXME: why not return the number of calls here as well?
 bool
 ExecBeforeCommandTriggers(Node *parsetree, const char *cmdtag,
 						  MemoryContext per_command_context)
@@ -602,6 +610,7 @@ ExecBeforeCommandTriggers(Node *parsetree, const char *cmdtag,
 	RegProcedure proc;
 	int cur= 0;
 	bool cont = true;
+	//FIXME: add assert for IsA(parsetree, parsetree)
 
 	/*
 	 * Do the functions evaluation in a per-command memory context, so that
@@ -631,6 +640,7 @@ ExecBeforeCommandTriggers(Node *parsetree, const char *cmdtag,
 /*
  * return the count of triggers we fired
  */
+//FIXME: This looks like it should be static
 int
 ExecInsteadOfCommandTriggers(Node *parsetree, const char *cmdtag,
 							 MemoryContext per_command_context)
@@ -641,6 +651,7 @@ ExecInsteadOfCommandTriggers(Node *parsetree, const char *cmdtag,
 													CMD_TRIGGER_FIRED_INSTEAD);
 	RegProcedure proc;
 	int cur = 0;
+	//FIXME: add assert for IsA(parsetree, parsetree)
 
 	/*
 	 * Do the functions evaluation in a per-command memory context, so that
@@ -673,6 +684,8 @@ ExecAfterCommandTriggers(Node *parsetree, const char *cmdtag)
 	RegProcedure proc;
 	int cur = 0;
 
+	//FIXME: add assert for IsA(parsetree, parsetree)
+
 	/*
 	 * Do the functions evaluation in a per-command memory context, so that
 	 * leaked memory will be reclaimed once per command.
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 2b1a76d..1505683 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -384,6 +384,7 @@ _readConstraint(void)
 	token = pg_strtok(&length); /* skip :constraint */
 	token = pg_strtok(&length); /* get field value */
 
+	//FIXME: what is going here?
 	if (strncmp(token, "NULL", 4) == 0)
 		local_node->contype = CONSTR_NULL;
 	else if (strncmp(token, "NOT_NULL", 8) == 0)
@@ -1428,7 +1429,6 @@ _readRangeTblEntry(void)
 	READ_DONE();
 }
 
-
 /*
  * parseNodeString
  *
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 83775bf..2a14281 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7332,6 +7332,9 @@ flatten_reloptions(Oid relid)
  * Functions that ouputs a COMMAND given a Utility parsetree
  *
  * FIXME: First some tools that I couldn't find in the sources.
+ * FIXME: replace conversion to rangevar and back to string with
+ *  NameListToQuotedString
+ * FIXME: missing quoting
  */
 static char *
 RangeVarToString(RangeVar *r)
@@ -7506,8 +7509,8 @@ _rwViewStmt(CommandContext cmd, ViewStmt *node)
 	viewParse = parse_analyze((Node *) copyObject(node->query),
 							  "(unavailable source text)", NULL, 0);
 
-	appendStringInfo(&buf, "CREATE %s %s AS ",
-					 node->replace? "OR REPLACE VIEW": "VIEW",
+	appendStringInfo(&buf, "CREATE %sVIEW %s AS ",
+					 node->replace? "OR REPLACE": "",
 					 RangeVarToString(node->view));
 
 	get_query_def(viewParse, &buf, NIL, NULL, 0, 1);
@@ -7526,6 +7529,7 @@ _rwColQualList(StringInfo buf, List *constraints, const char *relname)
 	foreach(lc, constraints)
 	{
 		Constraint *c = (Constraint *) lfirst(lc);
+		Assert(IsA(c, Constraint));
 
 		if (c->conname != NULL)
 			appendStringInfo(buf, " CONSTRAINT %s", c->conname);
@@ -7897,10 +7901,13 @@ _rwAlterTableStmt(CommandContext cmd, AlterTableStmt *node)
  * work from the parsetree directly, that would be query->utilityStmt which is
  * of type Node *. We declare that a void * to avoid incompatible pointer type
  * warnings.
+ *
+ * FIXME: just add a cast + assert at the callsite ^^^?
  */
 void
 pg_get_cmddef(CommandContext cmd, void *parsetree)
 {
+	//FIXME: at least add an assert for type
 	cmd->nodestr = nodeToString(parsetree);
 	/* elog(NOTICE, "nodeToString: %s", cmd->nodestr); */
 	stringToNode(cmd->nodestr);
-- 
1.7.6.409.ge7a85.dirty

#4Andres Freund
andres@anarazel.de
In reply to: Dimitri Fontaine (#1)
Re: Command Triggers

Hi,

On Tuesday, November 08, 2011 06:47:13 PM Dimitri Fontaine wrote:

exception to that rule would be SELECT INTO and CREATE TABLE AS SELECT,
and my proposal would be to add specific call sites to the functions
I've provided in the attached patch rather than try to contort them into
being a good citizen as a utility command.

I would very much prefer to make them utility commands and rip out the
executor support for that.
It doesn't look that complicated and would allow us to get rid of the
partially duplicated defineRelation and related stuff from the executor where
its violating my aestetic feelings ;)

Is there something making that especially hard that I overlooked? The
infrastructure for doing so seems to be there.

Andres

#5Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#4)
Re: Command Triggers

Andres Freund <andres@anarazel.de> writes:

On Tuesday, November 08, 2011 06:47:13 PM Dimitri Fontaine wrote:

exception to that rule would be SELECT INTO and CREATE TABLE AS SELECT,
and my proposal would be to add specific call sites to the functions
I've provided in the attached patch rather than try to contort them into
being a good citizen as a utility command.

I would very much prefer to make them utility commands and rip out the
executor support for that.
Is there something making that especially hard that I overlooked? The
infrastructure for doing so seems to be there.

Well, I think the main problem is going to be shunting the query down
the right parsing track (SELECT versus utility-statement) early enough.
Making this work cleanly would be a bigger deal than I think you're
thinking.

But yeah, it would be nicer if the executor didn't have to worry about
this. +1 if you're willing to take on the work.

regards, tom lane

#6Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#5)
Re: Command Triggers

On Thursday, December 01, 2011 07:21:25 PM Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

On Tuesday, November 08, 2011 06:47:13 PM Dimitri Fontaine wrote:

exception to that rule would be SELECT INTO and CREATE TABLE AS SELECT,
and my proposal would be to add specific call sites to the functions
I've provided in the attached patch rather than try to contort them into
being a good citizen as a utility command.

I would very much prefer to make them utility commands and rip out the
executor support for that.
Is there something making that especially hard that I overlooked? The
infrastructure for doing so seems to be there.

Well, I think the main problem is going to be shunting the query down
the right parsing track (SELECT versus utility-statement) early enough.
Making this work cleanly would be a bigger deal than I think you're
thinking.

Yes. I forgot how ugly SELECT INTO is...

But yeah, it would be nicer if the executor didn't have to worry about
this. +1 if you're willing to take on the work.

I won't promise anything but I will play around with the grammar and see if I
find a nice enough way.

Andres

#7Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#5)
Re: Command Triggers

On Thursday, December 01, 2011 07:21:25 PM Tom Lane wrote:

Well, I think the main problem is going to be shunting the query down
the right parsing track (SELECT versus utility-statement) early enough.
Making this work cleanly would be a bigger deal than I think you're
thinking.

Obviously that depends on the definition of clean...

Changing the grammar to make that explicit seems to involve a bit too many
changes on a first glance. The cheap way out seems to be to make the decision
in analyze.c:transformQuery.
Would that be an acceptable way forward?

Andres

#8Simon Riggs
simon@2ndQuadrant.com
In reply to: Dimitri Fontaine (#2)
Re: Command Triggers

On Sat, Nov 26, 2011 at 7:20 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

FWIW (scheduling mainly), I don't think I'm going to spend more time on
this work until I get some comments, because all that remains to be done
is about building on top of what I've already been doing here.

+1 overall

One of the main use cases for me is the ability to
* prevent all commands
* prevent extension commands - to maintain stable execution environment

Those are especially important because in 9.2 DDL commands will cause
additional locking overheads, so preventing DDL will be essential to
keeping performance stable in high txn rate databases.

So I'd like to see a few more triggers that work out of the box for
those cases (perhaps wrapped by a function?). It would also allow a
more useful man page example of how to use this.

On the patch, header comment for cmdtrigger.c needs updating.
Comments for DropCmdTrigger etc look like you forgot to update them
after cut and paste.

Comments say "For any given command tag, you can have either Before
and After triggers, or
Instead Of triggers, not both.", which seems like a great thing to
put in the docs.

Looks good.

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

#9Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#7)
Re: Command Triggers

Andres Freund <andres@anarazel.de> writes:

On Thursday, December 01, 2011 07:21:25 PM Tom Lane wrote:

Making this work cleanly would be a bigger deal than I think you're
thinking.

Obviously that depends on the definition of clean...

Changing the grammar to make that explicit seems to involve a bit too many
changes on a first glance. The cheap way out seems to be to make the decision
in analyze.c:transformQuery.
Would that be an acceptable way forward?

ITYM transformStmt, but yeah, somewhere around there is probably reasonable.
The way I'm imagining this would work is that IntoClause disappears from
Query altogether: analyze.c would build a utility statement
CreateTableAs, pull the IntoClause out of the SelectStmt structure and
put it into the utility statement, and then proceed much as we do for
ExplainStmt (and for the same reasons).

regards, tom lane

#10Andres Freund
andres@anarazel.de
In reply to: Dimitri Fontaine (#1)
Re: Command Triggers

Hi,

Hm. I just noticed a relatively big hole in the patch: The handling of
deletion of dependent objects currently is nonexistant because they don't go
through ProcessUtility...

Currently thinking what the best way to nicely implement that is. For other
commands the original command string is passed - that obviously won't work for
that case...

Andres

#11Andres Freund
andres@anarazel.de
In reply to: Dimitri Fontaine (#1)
Re: Command Triggers

Hi all,

There is also the point about how permission checks on the actual commands (in
comparison of modifying command triggers) and such are handled:

BEFORE and INSTEAD will currently be called independently of the fact whether
the user is actually allowed to do said action (which is inconsistent with
data triggers) and indepentent of whether the object they concern exists.

I wonder if anybody considers that a problem?

Andres

#12Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Andres Freund (#3)
1 attachment(s)
Re: Command Triggers

Hi,

First thing first: thank you Andres for a great review, I do appreciate
it. Please find attached a newer version of the patch. The github
repository is also updated.

Andres Freund <andres@anarazel.de> writes:

I think at least a
* command_tag text

Added.

Why is there explicit documentation of not checking the arguments of said
trigger function? That seems to be a bit strange to me.

The code is searching for a function with the given name and 5 text
arguments, whatever you say in the command. That could be changed later
on if there's a need.

schemaname currently is mightily unusable because whether its sent or not
depends wether the table was created with a schemaname specified or not. That
doesn't seem to be a good approach.

I'm not sure about that, we're spiting out what the user entered.

Imo the output should fully schema qualify anything including operators. Yes,
thats rather ugly but anything else seems to be too likely to lead to
problems.

Will look into qualifying names.

As an alternative it would be possible to pass the current search path but
that doesn't seem to the best approach to me;

The trigger runs with the same search_path settings as the command, right?

Then there is nice stuff like CREATE TABLE .... (LIKE ...);
What shall that return in future? I vote for showing it as the plain CREATE
TABLE where all columns are specified.

I don't think so. Think about the main use cases for this patch,
auditing and replication. Both cases you want to deal with what the
user said, not what the system understood.

I think it would sensible to allow multiple actions on which to trigger to be
specified just as you can for normal triggers. I also would like an ALL option

Both are now implemented.

When dealing with an ANY command trigger, your procedure can get fired
on a command for which we don't have internal support for back parsing
etc, so you only get the command tag not null. I think that's the way to
go here, as I don't want to think we need to implement full support for
command triggers on ALTER OPERATOR FAMILY from the get go.

Currently the grammar allows options to be passed to command triggers. Do we
want to keep that? If yes, with the same arcane set of datatypes as in data
triggers?
If yes it needs to be wired up.

In fact it's not the case, you always get called with the same 5
arguments, all nullable now that we have ANY COMMAND support.

* schema qualification:
An option to add triggers only to a specific schema would be rather neat for
many scenarios.
I am not totally sure if its worth the hassle of defining what happens in the
edge cases of e.g. moving from one into another schema though.

I had written down support for a WHEN clause in command triggers, but
the exact design has yet to be worked out. I'd like to have this
facility but I'd like it even more if that could happen in a later
patch.

Currently there is no locking strategy at all. Which e.g. means that there is
no protection against two sessions changing stuff concurrently or such.

Was that just left out - which is ok - or did you miss that?

I left out the locking as of now. I tried to fix some of it in the new
patch though, but I will need to spend more time on that down the road.

Command triggers should only be allowed for the database owner.

Yes, that needs to happen soon, along with pg_dump and psql support.

I contrast to data triggers command triggers cannot change what is done. They
can either prevent it from running or not. Which imo is fine.
The question I have is whether it would be a good idea to make it easy to
prevent recursion.... Especially if the triggers wont be per schema.

You already have

ATER TRIGGER foo ON COMMAND create table DISABLE;

that you can use from the trigger procedure itself. Of course, locking
is an issue if you want to go parallel on running commands with
recursive triggers attached. I think we might be able to skip solving
that problem in the first run.

Imo the current callsite in ProcessUtility isn't that good. I think it would
make more sense moving it into standard_ProcessUtility. It should be *after*
the check_xact_readonly check in there in my opinion.

Makes sense, will fix in the next revision.

I am also don't think its a good idea to run the
ExecBeforeOrInsteadOfCommandTriggers stuff there because thats allso the path
that COMMIT/BEGIN and other stuff take. Those are pretty "fast path".

I suggest making two switches in standard_ProcessUtility, one for the non-
command trigger stuff which returns on success and one after that. Only after
the first triggers are checked.

Agreed.

Also youre very repeatedly transforming the parstree into a string. It would
be better doing that only once instead of every trigger...

It was only done once per before and instead of triggers (you can't have
both on the same command), and another time for any and all after
triggers, meaning at most twice. It's now down to only once, at most.

* many functions should be static but are not. Especially in cmdtrigger.c

Fixed.

* Either tgenable's datatype or its readfunc is wrong (watch the compiler ;))
* ruleutils.c:
* generic routine for IF EXISTS, CASCADE, ...

Will have to wait until next revision.

Thanks you again for a great review, regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

Attachments:

command-trigger.v3.patch.gzapplication/octet-streamDownload
��U�Ncommand-trigger.v3.patch�<kw�6���_�z�D�h=l���ge�v��H^IN�'���HH��"U>�����wf��DY��n{��il���13���)����1�jN`�����^-���r+��8tg3V��M5l����Y'���=�V��S~<��F�~|t�������v*��c���������8`�sp���=0[#��c�~o8�����F����9`g!_x�������YQ���Mk�zc�����o��YMs�����������V�b��������y l�}�����-�w3���������4�m8^Gm:�d+�%s���*:;l�"�d��_��y����.a����V��/���7�^gd;��-�Ya{�����;[��V����x����`V����&�>���:�Ix 	R��9�����Pa`�V���y!��V�VA���u�k S<���6,�Ua �(�
�������R����)ZZ��2ZCg�������]�Gm@�*���Ffo����AZH�-�`�3�=U
��	�n���h�Y�yl�� 7r�1<vxd��Bx	�$���|�������y#=��v&�a�,��{��"������]�C������������h�mw�BSL�!��4R�G��gn'1_��-�	��0%h��l���32�����U���l����=�
�`0C.��C����fRK!O����'`Z�&dXRepR�V�K������i���)�D���������\?����i�v�������`���Gy���(f!���YM����o��v������_Z^lQ�����>Jy��;	���GGE!�U�-ac�1	@�Ha����T�A�����.#f��X�I�O���N�{��Z
L�q
\�
�����h���n���.����7
=8���?�}�B���q�T��Y��<��<?��7�S%���w]G����5IZ��+�4�������Z�*ub�Y��=L���ak�
���+(�PE+[�����x��V9<>1�0��K�0C��p?J/���;+F�:����@���������2�l3^.x��RNg�s��f�����]��w"%��e.�!\�|�2�B]XQ��/
�T.K�*
��U�$�L�f��X�?��V�z��&�|��V�"W�������A�+�nAW��F��m1)�6�jmcU�R�e:��D��i�������_��#��x��n��&KvD�,��]���������J����0�>U��v]���rn��[���T���	ZaK��s+v�K��9��}�AV��?'n�b�2	�Xg�D@�>��8������{��x���������������5�M�^�h� ��<���������s_*F�l�G��W���� 6wgwB��)yi�M��6�:�*���"�n��:�]�[D5�3�=��MrC"�jb���jr����x��aS�WT���sQD�b���l�`����J��q���^c�$����gj�5�� |��9��l>���q/=P�s0"���P+S^=?��^�'�_���D��������V�d���� �ch��eZ��c��K�B��7�>��?��Y����{i���.4��9����E��u�wu��2��[�����5d�~�S�0����v/!
�����;�c��W+?�&��{n�B���9������!w�������]��������g���+��sC��Z�#t�e�zhR�	�U��e�hp�]�����5�gE�S�����gek^/�g�0QE+�*�@��<����qh4�
�����������3s�S��*.*���1��<�����#���6+�m������� ��%D^�.0�"S�`L-�xY�����b�������RoF=�����y��K�N,����2�����O���!?<���-e�u0���)S��~7^��d�`�<=YV���KgZ�IT����C�6�GC��	�W�n[&Q0��'�vk�nu�h;0��L{�>�T~o�������X���Pypj�B\����YpO�S�hh�����O���0�w�U'>u}WkF�Vk�lB	�����d����;�{3(�>E
�>O��+�Z�;+����_�����cPe�-�SQ�b�S(�$06�y�("���|9Jj�
l�[�S�X�G�;5��Y����N���i5N�j���8G�q�C?@���P����
�F�����Y�/���!��}[�gD���9�@7�#��o{��S���VZ������*�u��
hZ��w����zR�c��}3Bx�p������R���k����u�l�g����q�Y����n��B���H@3��fV�	�>��p{����?K�J3���,�fW~�l��Ft�~JD��H.�#���R��0JL#8���Rz�;�T�����h�/4$
����
����sf�H��30v>�Z�Dw���-O�Vk��o/��m����������T�M�<�����&d������3�4M���7i
lW��-aw���M�B��;��^Ma6B<��<b�x�����wE&@Q]��!��8��M��:��]�xn-��y^�,����x���ng�7��S�����	:�F�������f�$���P:�Y<z�������n
����
t����C��d.�u�R�#���u�z��9x���L�n���'i�na��M���U���Sr���m��1/[�k�_2U^��-�c���W��o����>�~�Q����5FL��u�o��)C7'�p�#��d<1��8!�"�B�`8?��[_����Q*�V���+�*
(p���Xv����/�]s�ra�01�~E
h����	�`��i���i��C�
M3
��U[hN�)���J��
��rn�����3�!_hujt$��?�uF)H��{Y?!�^6N��F�:-n~�\YZ�,�_�wI�TB���
y������/;��X��/�5>��e��8���e��Z����Kq��m`��q�MS)�[JW�p�1��E7X����;+����q/�!Q�B ]~F��?�~�R�Z����{	n������sbyCp�1�-e7����_���x\I�����$�DE�HN������H64�o�������g����5�{����C[+Y��
��s\(d��)+�����^&����8l6es0�����t�
��8�2�������[V�2A��u��cW�������I����pcH���?
��&	V�
6.���A%����=�w�Q! ��������G���������`�Z�&��� ��h�U3�
*9KG�K�m�x7�Cn3����)�w	���XiE���:m2�K8��q��(j�_�ZV�z|2=�Z����*�����H���`c����W�9�hr��4�Q���5�D���G���������%�,�M�����:�~P?���Y�)<�M��s��=�|�;8�D������q�F����p��wh~;��gl�VJ��F�v�O]=T��u������^�����*m����K{=��'��T�V�n6J
� X��
R ��oQ2q�7���;�)���1�Z��;L��(��!�������`by���/�����p�UfA��_����p|���
z$�TV0�u��+AhVJ/��������|
�)�R���a%��Y��v���'0
9�7����,N���L�S����V�w�9���X�+�F���j-�fo�7�-��><hU��z�9>9x�R+���*},��d�����S�0���#{��=��`k;Q�Td1����"�\��%f�0�����@�^�4�8�]]�8]+uZ��Ekh��H��RYy\����T~���1;x1��<� %,�_?����}xz4!�������\�qnt����2N�1����^��^?����,�&4��~���T�ia���j
���T�
�������
�4hW�Y���~�.�]�A�HA��u/���H�)!:�Y%������Q�B�@�CE�7|js[���D�A��5�
�����W�p �+(���OB���c�Q����u��#����"����1n��3��Vc��jILV��������C�\�9����u�;=����xa������3����X!��(�N��(/�d�cY��!�c/�<�G'��B����w��|v�^R��R���1l�c	w�Z��*�0@���(�B�������+pr[��=��/Ha��H9vE�	��+���� \�n�"J�a��6�@�tW|V��~���G-���p�t�'��'WeT]���������a!�s���4
�~���C|h!g0_��R%>��Skd�aP�+���
U�G,��i�r�N;���w*�$az+
f�8�(r�?/��FV*�G����h�y.*&���J�Io��y����x�L]�%H	�s�I������8Ui����F�i]�M�K�f�_x0���#qo��J�j����>k�q������!���.�AP�b�~H�X+QJQn�B��%�6�=#E�+�'xq���f��]<\���}h��$�EmQ�����Z�������g����&D$�F.���Lq�N�2������o�e�l��h\�^��\�\�r'V�B�����&-��x����EM5)h�#I��&�vP�}��~�����e���
~�&n)g�
���~�V�w�����l

����x#5�u�	��)�Ej���M~m���T���b%!�NJ{�/?S��Y6�`���Ax������i]�)\��M)��9�6}<��?��Q���7G4R�#3F;FK�r
Y���
~Nx��_�m��TV$���6�<p�/���FEZ��Y�J��l�\A6�k��,�"M��Mb
@���wM:��?����?|y�A�$�5�26���J8��]\���I*�Y����'#����� ��,��}��[�[P�D�/I������ ������l�}�������C�yD�������{N���!+��&�����s���2I���b������$��~0���e[e�jS��$��q���<�*��{���aCJI�%)5`1/�Z^�L�?����
�gr+����p�=�*��3'��[JP��4t|.�?��e����9����`g��4U	&B^#��������?��b�n�5�1��}c���c�K���=�*{��)�L���=���
6���b
���q4��p����K�Ry2���p���	G�����	P��_�@��NM��89�����,�K3����}�����\8%ulL5#����}c��m���e=���6���yH��o:c=l���gm�(M-����>i�w��������SJ>C�Ey���eP5��`{��1��fkt;0a?���77����h�����hV�}��]�~7��;+*�(/�5��r0[�W�m����$��g��������/w��6�8O�x@z�����@Y���N��?B�OM��?U�R�5���q���+�A_��i�$�*'�G��#�>��u���g#\�;�'J��]����eI����W�/�1Y���
�R���(6�2��3�G� ��~�@!�B�I��{���v�������X�O9�� �PR
C��b�w����w��9%�1{"�X	��2����o"�-P������.��=V\M�����SV2��%_T�//	E����v������4�������Go!c�6���\���	4���.OZ2cD34X�$���,�vW/�v&s��}'���SUg��,�2�=����������~u�o��uOll������$��II������Xz<�R��tH(V[���:�7���d�����,�����8����3@B���uh�!M�����fu%�vZm�N�W;+��mm'?M����� ���S2��N|�hj{�����sZf;gic"��-Ib.Q/�-��q��oB@��h�N�}���1��m�lP��*N�&��s�fL��z�Qj�k�P���mGga�l�Vj��s�U�mUK��T�
rJ��g����Yj�Mj�Aj�-��
Q�9q��.�h������N�mS0�j����Q��s�B7G��AT>��F���1��]t���3���'4{��y��,����]j��E��S���7���Q>�o��&u���[�Z0Y/��A���
9/���h�!�p)��_{��~������O?E����l�{�b�C_K��f���?�~}�$�[�SP�d���*�*������f����.1�m�g*�R|N�����-6�Lh�#Y)��}4��ST����&�06�F�����P�X���,�!���K�O?���E��#�a�fY��f��9�gA�~M�hT#R6Z�l#F�t�`�\�J>
���vc��i(R�\�pK�L�;3��1={��a��V�+4E�Y�|N��v�G�T�|wF���������|������^3D�6b��gdJ�O|$$�{�1�����~��Z��h3�^���ya��U�q}9�/F�� ����,��!F���8�1�G]��_-����7��/��-�=�}S��C�)�m�{j��|LqZ�/�uW@��&��b��=�{���0"=�e���{�O���1QvI�F��WIhM����NF���X2��*J[�I�������d�O�R�����}>G~�
 �x����S��;$l��B��V�[��9����0^����W�sm��CiD��q���T������J{�PK���RZ��U>Wb��e��0����?���2�p8��m
��,�,�B��H�d���RH�w����mU|���5P����m����������f�|��5����������e���<4a_y+#F������|���yR�d�4f���`p��Xq�b1%��x���XD�	;lC�:���231r_�YX1�����^jW���"�U���v�+UX��ec�h�R�ZU�h����=�C����9���3�f���0PF�1�X�B�]l���U��0��u��(	�
�\W���>��WE��m�'��0\P$cdQ�w��"��L�c��Q��-	�2
�s��������0!�,��� S1QC4�f�����f��5$�`p�(�S�PX�K�"SP.����e�x���:��f�k��4�)S0�">N'�x��7�^�L��]z�^�����qy��I^.���4��7����	S���B�������m��@L6�F(3���X2`�(���,8���@)��_��/{x�c�(�O#3q�v����g�g�� :�L��B�X�~��M��4a�r&�{{�n�
{{h^��D��U����d�����y���=�E����b�d�
����g���~���;�E�
6}.}K�*�2�J���z�^�2Ld\����
Q�o~��S�8�i��s�b�x��r�+��6%J��J���N�)Y�&���B!��f��j����Q#�������*�bM\v��}�3����W���G��`9����5"�_�\J��\T������ x���y�%�gIfh�Lm�;�Y�d]t?qQ����A�*QVW�V*
8�?i�X8IbD`��)��7]�{�W'�8^�]���n����[#!����.0�|��;c�]�
�/�A�M-�V�B8U�������#�����f�\9�8��m�k��a9�<���E�i�\���s�-�8z2*~s��HH[��)��w���`2XD��v����0��]��d���Z���0vL�>�@#�c4a�W�
Z��%5p��h�{h��M�9��P�{}q!xp>���|������U�S����f> ����d�K���'�1.��B8���S� '�g�f�:��j	�(
C��fU����U3�|���������UgZ�np���d
R�x�/=�t�����@���HVv���6����r���p��L�z<��~}F��������NgWm��j�����������Lu
U��n�|�Hg�#�Q���EF$i�=�V��c�A��u:����~aE��v�+���$��m�}�B���D���
�\bq�pf;<������E=�H?we��hM�5Yf�S�8�������������)�_��*C�,�v�l��d5H&�PI-M��!aJc`(���:�Y������U����LBJb��������V��j|%B3��!&��:pi���8��������IQd~c�M=���d��l� \����������P�B\l��������D���^k�&[����pt��:���[��+]6�:	����(�����M���G�u-�*�h���f�N>�g�����M��������Hgk������b�H25�R���P���_I\S����VLLoS�:8��(�g����;��d@\	E&��5n��FH#; �u.��26�D�7{{x���;�f�L��9y��@h��y��h�+�z����f%�?���7IKJu:|�L��#�@%�������enMB;���(&�M�����B
�p����z�ZD1��H��V�g6��Y�t��*�Yn�^4S�e*?p�M��
�SB�����R����8����.����h�]g��~�6���)����6���C�����K��8�'��fj��o ����o0������=�W6���
M���t����w���e$h���
C&L��
�Tur��4{
�u������Q�etj�C��������fl�VCKC[`��K���VCn}� ��["b����9'~��I��3����#��k����KfD��Q�����'8��	1@�]���v������q%I$��MbB:�G�,�f�Iu������c��I��"v�=P����R��_6G�Y�����O��}�QVj{��aP�D�]�;���hr�q�F�g�|QOc���O��n��"��jX����J��r��;#���	�h�??��e�V�����5���^����Bi;�|������G7��Yi�Q�/B\���I����q��R�*��~pt3���_�����K��U��CY��("5�<�8�dX��M9�U�?�s����Jr���g���H�	}�U�|n�l�:OT�5yB��B9�{�;��S=�x}�+���):����
��F�X��<�oe�B�`����n�>�Ce�����/��]gpK����<�y"�I��J|Q�,zJ= L��a���J�.uN�� #@�yvG�R(�T@�l��I�K��"�$�|�|�s�����Ix�x-��=�IV�|�q���n���sm�.��Z�����J���i�rn��}Jvx���/b'�O>��~F.��Y����|X�:p������M�	��K�Q�����������,y]&�F��v�<L��Rg8�arC�g��9�-) �!���u��$������	�::::8�=-"�NY���D�SN8��<�t#�T@���H~��e�.u2�V��C���7�8�.��1*�Jsy��5ri��N�zjda^�	��RZ����K�TH R��������������C��������?~�� �Y�qpAU����vCdRd]<�W��i7_sf��}IGi\IB�w���<!��0��������d�O#�H ��Z��hy�S�YN�X��z�Sz_
�i�^�SS��������Z����������ZdR�
��S��G?���}u�f�|.��ep�Y���l�0�"Yh���x�9����2�5qv|�� 9/S�fDx�]�3��cUV�;�A�?/���XU<�:U:�8�~�$g�L
�~��9���WA���m"w����7^ ��| ��/���'a�4�L���/��������^`��|`\_�7?	�`y�r�Qd��X^"5���-��E�b��O�I��&�~�����������Y	��]H���]G�{}�n��
���5���E�4�%O�[��A1v�S~o�y\c�zY��G��t�������{�M|JMn��mL�J�.`���@y��~��K�����zh��7�xt�}�'��i��CB��dU�n�n�f-�0�l9�A�����,�3�i��Sj��JG�"q�?u+�Q*rJKlI�W��xy�����@�J��Z��dPR�	���������Wzss���j�k�/�W�������� �|D2 ����/��	_�k���'�����7%K�F��G�F�7Q���G�U���%��~��J��;�Z�\�=�Z��
&������`B��P~��"-L���K�m1j,��s���z���������+j�SJ�.[q��%����G.�*�����h6�W��l�H!�2�|yg��1
��,��{��S��<�+���dF��>%Y�=�����3��@��f@&S�:��+��N2�ot?�3c�y|����p�2w-�;o�C��������I�����:��{��kdA������)L�����Q�W�w�Go?�����X_�!���������%��t}��bH)nC�s�'���3�0�$��v������zz�G��Q=8PGu���R�bid�w��*�������#�\���.�a�Q�Z���9 �+�/��3�*��R�zs��s
��<���U�n
FLYq��L�:e��<~5.$
�+��V�[?h��/	�Ax���!�}~�o4�_UO��
�l��q>x{P�;�c��.w�?B70�M���'[������$+�������E�2�Ns��*�U0�����e	����Onr���e�}�}z��M��<M2�����{���E�����LO��f"CS��1��Pz�n�YU8��[��\�1���,@,��e��h8�E �w�lKH V�����tt	n�+g������U���?��Km��\]4�o7���-�,�2����,\fO�����e��,vs1�!�� 6 ��<1WW(����f�M�B,d.X��O3(*�n��1WK���Y�[�3"p��Tte��h��A^�U�Y/����NR
��6��+���2��Z�D�|�����h�������7,�13�G��
���������X~/`>P%������fS�����$Up��sz����������5��@��j��O�DP�s2�������b�H�	`?���q1�+E����j�X��O��A�����K	b�_V���^;u�\��O�v����������v4�V#R�T-g4[����{���@��������zS���8����4����ML�el�z�����,U#C�T|��7/�C�4�	�DBh����I�*4�e����~�p,?d��)���~��e��W������+D��W�h�/P�px3kc|���vi�n�,��jX���H���<G����c����m��+����[�l���4���{�l�t�I����a�q(�N�������'�g3����d����5���X�M��
��wd��d������G2=�2���;V��Oq�����2�)��5�zI��l��FoGh�Q�#�'{������#���1	(�^�J'��o�e��{�M-���[<��Zjui����C�U��w���b+aVi,*��rw�������U��L�I����}��
��#���^YJ�\����V�*e�M�68[g�'���u��r�&�xi��=��B�xUJ�l������1�PkA���xEJj����%�$�kLAc#<f��R=I�_q/F�f����������n\MV�5Yf��e�P����#���8)���jH��PW���4n��,����������|,}g�+�LWx+�#��c�E-"e�����cv�>=��n����m����a��},��%�bx8�5-W,���);���G��g��(���;bly��b�0�oM���<@�N�G�z�	���)����x��Z�Z���*��h}�
;�*C������F;���o2���b�+�I��&���m����E�*vY+v���XrTm����*id���$hs�j��
�e�����V��q��v�|t�k5/SC%*�����d!�#)5x�tjv�_!�E��������t�E�k��d���tU�y�~k��e�r=.$^��l��j��y���$s���Jn��'c@$wI3�{��3v��$J�!��(Y3��(	��gf<$�wp�R���:�(;U�	f*P7:��/?IF��{�A��e�:#�Cz����sl�[����j����,�]���W�>d�lhYx6���RI�snY%������*���,��������}4�F3/�����()���]�j��C����o�
1��K��L� ��W�Fs�Y�EU�90�L��X~$ h��^�/����+���!�����;�O���*Z*��!F�CeP��C8)��X�$��:�nRv;y���t����['A}�>
�F�Y�g�K|���~3�@���������@fK)�����g�i��0�!���
#����� p3�����C�a^�Uff���{!0%����Rm����a�5�XCfb�1�1��Q��pd7;��������������|��w�������������0	MO�^�����||�`;�����s�UqH��)���N��w
���>L�G*�fM�{����U�N{��~�t{b����k���VPOm[��OXAW�=o~����n:;j���v��\6�?�����s��&S�m���>����=���uN��@<y������]�aa����[�����	�����*N7Y�n�]B��x�����-^:�<�����5,>�4�J��`�Zf,�.��hK�W5{���^LF���[e��Ul&��H�eV"��`6{����nU��}��D��E���g�� �p�:�h,��Y�u��q��'��}^<F���}�ii9�
����d���S���|���9e`�X����z"���
rq�3�a�R\�P\� Z��.����\,x��7��s��K��^={����R�,W
+5Y����zA�-���E�'k=�����W'-QUu��\6��*�����7�vI[�N&#����I�
��	fgek�E6s�J���ai��i?0d�2{������atc
f�jM�]�,g����~SJ	o�h��	7��������R���j17��cM�M���I�cNv4���o���+i(E�1�`%����w��y�����e*���/_�-���cB���IEv#8<=��k���0�����l�uZ������G\:���e��1����LWq������b����p3�*y�*�_��o�����Q1�6���Th[���lkybV��_��A2f����{�������J��A��(���3m�������:}����?���B����g ���A��[2�po�&����Y�_�����6�A��z���he���=(�:�%h�'�^���X��r���(���h�u;�?/��b:�[*�(�L����-�z+���4c:_�=R]!����z�=�����
��I�;���Z=����h�G
W���^��E��P����m�OrG�E��������t��'�G6}�T���Y�Jx��0Au�x.�s�
����B��"
&���K-_F9�vH����K+C���/�$��`�m��m�7����7���K�NWF����.��e�������,�A������uzrE���	aJ�D�X��;�P�#��A��D<�H�>����G���}4���u>
��=E�&��wx���9���l�����E�~Z����I��!�g����]	;]A��v�4[��E��;�� ������6c�/z�6\o[D��E{��qz�b���c
�����W?�Kg�QN7��g�E�J��!���m]���^`c+���m������~PG]`��*����n���W�������Uc�~a3�Q���x�O���Rw�~u��������b�l�
�����;����C���h��@���HW����8E>�Z��fthU'
2H:9�W���
,��C���"w^�����STh=����<�C��9y0=sM���Y��H�9A������D(�)9���)!���>�%��>���Q����H^��mm���i2{�'p)t���J$j��L������rRW���(�:m{�d5�������l/#�ob1��%�a����	z�v������~���V�����iQ�Tv�K�b�����K���B����:����w��|u`���@�����]K�~l}Wq7������W�����;�_Q�'��o��4�����]8J���e����mX$����hw��P��K�A���%*�E���?+g�_�a�u��iDs��{��Nu�`�-��Lr?
�� k`�~�����g_!I'�u`�������3����H�u���PL"���h�RY��I��u6��Q,�8��7I�p��(^�-���.�?�
�h�����	�P`(z���/f!z*O�2�wd�,x�����G�	�^���n��]�d�Q�Q�2��^b8���]6�7��~���:nNi�+o��]�}�{�E��4zV����6`��|w���1V�oJ��3������-���N���3���+���7��C�XT�{���j4��F���E~�\FY��_IP�����1�2��	�m��!!�y3B0��1��@�(*�2�r���aS�(@X�_yJ��C����'�����u�	�O=D�z��Q�C>b��t(6=��E�R0��-�~���{s a�����_f�����ApW����E�;�r�XOS���� ����N�iU���5/�5��U��3�w�P��,�J>�X�#�6���=(g
KM�����������������<d��o�����_����?(��R{�(q_1���&��JJ"6��'�uN�YB�L��.���Mz���vk8���0�"{
d"��W.���7>�c�HT`0��@�J�^<`@�*�\n�����q�x)����[(_N�o_���j�����4��4�<�t����?��C�g�����l
���G$DU������q>�*�[<���u�����:�x��<��"��r U�����p�:bU*����_��������1����8e���g}2�zIz]�*�P���Gf��.�O�|������`U*�$�s����;+���8����+L�Yy��:�����:��]���5��T
w��@g��x����#����_�H=��pNZ����Pj��P<s���T��
��8;��G�|^%p���z^Ja���6�p��@�����l�h'�.0n+eZ���x����'U�]q\c���=��
�P���n&t��c2mD�����|���M����*����A~��@���1�7�?C�-���KT����xU;$�/i0��%�"�����f{�j
�fU�
�`W|'��tn�{!��������L'N�Q�����YL��b����cT+q?h���f�]�C����M	'5!k��@����G�{4��K�k����������xP��Q�����=�������i<�p�fUA'�=g�%��y�8�����#��?�������/k��kI����U�w��$r�8���Q����~&�����>�H)��'�k���������Gr)H�'?�NM��`����.���A�����~zPg��c��1�L|tg�1���n������(V�������H(��eW���U�Q�
1p�u�m{;���AC��?���o�v���������v=#zLaz���n��6&������V��S�c���������������k���A�]1>J��F+�y������)�0���Y���zI�TiP�;��>��F�8<9�6��m������W�!4?�\���u��<�������)�����{��7�����'8|�R��m#!��?X�:9:m��Z0�9:9g�\�()��_��i���i�^O�`�����5�z��O�._��Pv9�&JHa��6��������C��-S��,�#d����<���r���i��)�K�(�g������VB_���gK��P1#{��*���3�>E`'�e_�_��������}��t����������������v�{����U.pL�?�.��9<	�'����8��q��o�i����W�	��D9��w?6K9lu�5���q��N��#�/9�2����>�|�"��<�h�X�p\E��<"���G�����t�~-�L�������(���:�&|Zk���kv2��0R=S�=�M'������-Q����h9����0�~V�Rq�]��V�'3�g{$��,5%Q����e����xz;
'#��R"��9�WP�t��t����^C���z]�V��@������_���w&b8z����g`d�����8�����HD��	�����2&��P�G%��f�Y��
�����JG"Ru�|���I�E6G�'b?���P��9}*h6h����m%V_{�\f��"���7�T#(��?t����m#��%��NZ��kGS���A������M��Ld&���(�7�[��4UJ&���<(����~�#'��
A����@�y2.:�C��#J��w�<\N���:�$��N��d@	r���h�Mq
�a{�E8�x��p��BZ���1�����p>(.�����9�#��4D��qZ������H�/8���]<j�EQr[��}����'#_��*�{�}D������}j���"��|�2=T���t��;F%��=����VZ����`��1���b�	��RK��:�� `��}��w-Y�"�FbQ�p&���7��^Q�J��R��`�K����%�G��
_Y9�jp���Y�J���pl��8��rf����I����������<`��Mp����%��
�p���}>��!5���, "�w����3��`?�n�b�}HK�]���C��������w����7���;�}���3��'f�9��'@���f
��������YV�J������K	%&T�����2��*:�
�������q����dA�`�j���(:o������dUc��-���r���W���m������i�i&SC?��|�y��[Q��B��l�4��4$�&���rS����
��d+6�E&gcM%eE%Uv\:BT�)c4����q�0����L}��3�mE���+��6y{d��N#K;h�w�y�0��]�q%f�����d����{�y�z/^���a�l��2�`��K�M���}�����g(Z>�>�,�W(��_]
�D��G��*�	�Xj�<g*T� ���<�Lg��J����6 h�v�D
n��1�{�C4"y���nh�OMM����b��!�d�@-���U���.�D�'�SF�y�%N��U���:?\��u~|���f�~%�>&�V�9WTj>\>�k�8;R��pvT�1b7�C�m���N����VD�]��)0	�A=D�������!�^�����	y�S�Hc��L���0KF>N�W��Rq8���d�+��������3��A��V���*��\9
�D���i��`���FsJL�M%���h#P�q�L��.3�������i�@�XW�D��c3%��b23Wl�)yC>�.���9��$�?�f�Oq��'���c9�����H-�-���b2U�_F�/��l5%��j�q����
�G���	�B!�x��t����[ c����	�L��M@�����w��PTv�E�)�O�����`E}�����Q���b[`X��p�D\�����������V� n<���V�w��v�?��\�:pZ�L���a��{�@\ �Xy��b�>/{�t���HH�����19���9��NJjF�� &Y��+Y	g�+�?B����$oG&%�]��&�;'M���Mpl���h��������f�E[�}���M).����5X./�C0�j�ww���B����H����L����_�$|�li���Q �?Z��������������QL��M��I�)�3�L�dB ��rE����x.z����6�z]�[J���0�����3�/����SJ�!��/$���oS;�%�&r3���[ecE#�_ ��O[c���M��kH��oi,��Lq�4u�Q���w�J�k
����A
7��B�������8N�h}������C��������>������
2���0�rX�*:x�0�Tf�%��7���R���E������)H��!g�,J2�_
u6�]^GO7���-����)��	A��iqc��V�ud���7H����-����n�r����fNRv��o|��F�.���PL�7���s��w���w2�pe�%5�8\=e.��Le����8~�1�X��p:�as(��}=��l��B�ce��0|�
5'�9{��>Y��E���M&\+B>����v�����[��e�QwgJP�95��dc���������HmW2Wv05�������o~���/���z��/����Y�(���Q%�d.��u��)�n-�1�����M����G��:�@2�{��(�}�y�9g'����:P H�%��5�n��������)*�:���VFK�,�T��'���� ��:}�)�e��F=��z	N��s�I���X�d�6����!�?`��R��!��&�e�D������B�����j��0��Y��1}A?O�_-����.F	�mr;e���F�*g�[<�Uw�!����r�Ht�u�?�������=���H��-�F���M)~�rE�p�R������y�6�� �.T���G�t��\}r�m� ��@�Xp�7E!��6�O�9{c9
3	=���$�n<�~��M�AjN2j��&�l��e���V��d��
g�l�1`pM-	%v�yq��ii�������u���h���1Z����2X��������U������1�z/���?������S1��@
#l>��SqAj6�:?(v������Dl0����{`�V�Pt�?���!�����'���>�r�2��b9��L���x~V�/0E�_��@�����
yj���������$w�	$k
���nK���c�${�k,���g������5Bz1���{�^~���<�������]���*�np���^��`>1��:���`�H�L<��*��!���#�F���?������������S�L _|'#O�K	�����]
EN~p�a0�	'/��Ns����u���8
�D��_�������
����1q}/:<��������0	\�����D;�Y�c��[�x�79�aL��YP�@_�^:��E�as������8Ae#�������c���l�b�=Y������"�?��"\����v2�A�?������d���@db1�G�u;����;%>�f�T=�[��P;2��r�0������v���/s�U<i<�d������FM<c��	������|3�:n�b�M�p��2���3�������.���)��}D�z���5����|���*G����)��T������rB)������^����"�\�7���Z-8i4���t���NL���j�����&s�9��R�k����R��V�r��P�]6��0�m o�s��=����`�6>��.�����rOG�x�T��9U]5T��7PV4A*MM����l�4��������l~b����a��������Yi*��z����8����������m����D�����d����C���~[����l����b���t?�����h����H�LMV�?+�h��K������SAr���E��H�PI�������
#�d�n6}4^��p�cwU��'j>�)��d�8�S���_�����^�L##���C���6h��`�	�y&c;�0>�[������uv��g�+���uA��/lp���A��A8u�����Uf�6�N����%�Tx��g��D�g����P�#����o�>���&�zcL�������s�ur����}���#��������O!�Q1���qS�����<�������JR����=�Lorf��;��x���-���0�>����#t�~%*�]Q?==~����2y;��Yt=�[t��$�tX���/�����<
A�/-����iel�?�2t{ a�=�07���L���t��5�#t���X���l=)xy�0�����Z����g="/1B��Y�����`0P��	+,a$��!���`[[.�3���5^,�}a�� �M�|�z�oTh�7r���Z_
�*�W�H�)�'�s�xK�(���,
1�&)g�}��;�����QM�$�IV6�=@��s��n�q�^���`Q�e S)��s	6Bq#�����x�TY���20}r.,�
F��%��w���2��!v��?]�����{�����l���9,�@l�����F����*F����������I!�Z��P�?{�<=��������D>(�o|^��
(������Ti?�b����E5�B���zx*��AQ=%�;,�'� ����2��k M�H6��E4O�^_�R����'''���;�E�_��{;g� �%u��cp>�.��(��_� ��a)T�����>~��J���c��[J*SlBjM1�U���AjS����-����Y����������(i�!�����|��������`�(���=�@].Pq�P�6���7���aRwJ#z#u���������N6@��;�D��Z"9����E8��kU������6����N����(���&�����.�GjO�t����Q@e;����#B�Hy��[O�b����L/��[k�S1�=!�1�������mw�t��s�\!v�>�2���L(V�O�t5�
�[��|K���9�i��)�;2���KN&F;E����j��!�N��*���#!�
x��.���#�13z{�N\j�!�Tx)���r�wr����������Q�z+�C��uH?yrT=*	?8��P���
W�oO_g�����Y[��E8�[���k~|$��B'��#E�E���OaK�45��B������X�Y�h��Z)1�H��i'7��,QI���� `��G�'j��Z|�A�*�na=�r��(>a@D��b�������ln=D����	gO-��x�~;K���)�}���o����;ebD�������t4�_��]:88��d�%�u���� >�p��#������3��n�0��U����
2l��u?�m��"R�n��G�w��I�����(�vg�wPoTOs'��L�1x=XM�q��$'��0�����[U�'SO���@�~�����5�(�x�U���]�������Y�������|1�zz�H~�Og��5����:�����\-����R���TH����
��cJt�����A���R�:�����U_��*�Ps ��p��S����e�'3o�z�Go�'[����	e�9>*�%��Vk���-��5KL���iK~L�cU�d�R��o�gq���F����y{ *�|�[�\��^hW�h�4l����j������6,B�NY���DLK}�_U�g��*�!���q0u����m�������$v��b�f����lFr�rH�a�g��i5C��T�?k,��u>(����J��q@�<iV��*
�`a|uu�P�����q�d�1���}�2����u>���k��O��c�r�jigu%5pM9SG������15):�N�:7S�KMb"��=1pO����a�����X,�
;���9:^� �������k<��B���#K-K�io!���sI���qVT����<q��u�����<��2���y��X\�{���u�y�S����B����Y�
�,'��������P�\x���e�=-�����:<��|2����}��Sp��"���h�������Sh��u���_L�V�D�)�`�B��4H�TV��Z��M��$�q�������<Q����<���X-���+�R_U���Wc���	����._����8�F�	�7�Z�R�W����L9*QHal��kIj�������p���t!��j��%K�"�Ete��!-6���F�'��<�n<��+�]	�T8�%D,d�h��d�z*���JJ�_�R%KO�'�7�Z�����t�c=�l����5��"q�����������]�^�G�,���v�^��=h������Q�v�%�`�\n���@?������Y�A�L\w��k����������K
����2��6m�K��M�A�VS5P'c���Z����2�C���/���e����%�����������
A������o<�E���!,���)%
�����/]~y�4O�o�Kk ����M��t�+!2KQ�+�K���������
��-U���������n�]Qn������~Z��:mL��n��U��:��8��3��ORw{�N��Z�pA<&!��5D��R%�A��a�s��������,�B�Fb�GVMb�8��8Z�W��E�� �����Yo�i:���
�O�L��#��Q�Q�L���+�.����v�����~�����u�;�����-�����5�����}�~��bw ���b�L���z�jM��������@@1I>�$�i�������j����,z;�_e�5;���p���6~On0�A��
b����Y=�����2h�M�]:���K���u�m%G2�n���)Z��&�����@Np��M�"��A

�p�����*���`��#H�Aa�Uc�Z�{�QT	@�\N�YU��=�fz����GQ	�;�	k������Fc���(��}��u8�b#�{�$#I����lgu������*����� �H����}��0
��������YF�n-�T��Y�cd�n�F
��g���!����$�H"5��I�v{zx89�/ b�^2�W��#�u�\�����<�����"��*X�'A�2g�T�?t���Ky�%��?�g�hA
������7��)�
#13Alvaro Herrera
alvherre@commandprompt.com
In reply to: Andres Freund (#11)
Re: Command Triggers

Excerpts from Andres Freund's message of vie dic 02 19:09:47 -0300 2011:

Hi all,

There is also the point about how permission checks on the actual commands (in
comparison of modifying command triggers) and such are handled:

BEFORE and INSTEAD will currently be called independently of the fact whether
the user is actually allowed to do said action (which is inconsistent with
data triggers) and indepentent of whether the object they concern exists.

I wonder if anybody considers that a problem?

Hmm, we currently even have a patch (or is it already committed?) to
avoid locking objects before we know the user has permission on the
object. Getting to the point of calling the trigger would surely be
even worse.

--
Álvaro Herrera <alvherre@commandprompt.com>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support

#14Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#13)
Re: Command Triggers

On Fri, Dec 2, 2011 at 7:09 PM, Alvaro Herrera
<alvherre@commandprompt.com> wrote:

Hmm, we currently even have a patch (or is it already committed?) to
avoid locking objects before we know the user has permission on the
object.  Getting to the point of calling the trigger would surely be
even worse.

I committed a first round of cleanup in that area, but unfortunately
there is a lot more to be done.

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

#15Andres Freund
andres@anarazel.de
In reply to: Alvaro Herrera (#13)
Re: Command Triggers

On Saturday, December 03, 2011 01:09:48 AM Alvaro Herrera wrote:

Excerpts from Andres Freund's message of vie dic 02 19:09:47 -0300 2011:

Hi all,

There is also the point about how permission checks on the actual
commands (in comparison of modifying command triggers) and such are
handled:

BEFORE and INSTEAD will currently be called independently of the fact
whether the user is actually allowed to do said action (which is
inconsistent with data triggers) and indepentent of whether the object
they concern exists.

I wonder if anybody considers that a problem?

Hmm, we currently even have a patch (or is it already committed?) to
avoid locking objects before we know the user has permission on the
object. Getting to the point of calling the trigger would surely be
even worse.

Well, calling the trigger won't allow them to lock the object. It doesn't even
confirm the existance of the table.

Andres

#16Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Simon Riggs (#8)
Re: Command Triggers

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

Those are especially important because in 9.2 DDL commands will cause
additional locking overheads, so preventing DDL will be essential to
keeping performance stable in high txn rate databases.

The patch now implements "any command" triggers, and you have the
command tag to branch in the function if you need to.

CREATE TRIGGER noddl
INSTEAD OF ANY COMMAND
EXECUTE PROCEDURE cancel_any_ddl();

So I'd like to see a few more triggers that work out of the box for
those cases (perhaps wrapped by a function?). It would also allow a
more useful man page example of how to use this.

We could solve that by providing an extension implementing command
triggers ready for use. One that allows to easily switch on and off the
capability of running commands seems like a good candidate.

That said, with the current coding, you can't have both a instead of
trigger on "any command" and a before or after trigger on any given
command, so installing that extension would prevent you from any other
usage of command triggers.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#17Andres Freund
andres@anarazel.de
In reply to: Dimitri Fontaine (#12)
Re: Command Triggers

On Saturday, December 03, 2011 12:04:57 AM Dimitri Fontaine wrote:

Hi,

First thing first: thank you Andres for a great review, I do appreciate
it. Please find attached a newer version of the patch. The github
repository is also updated.

Andres Freund <andres@anarazel.de> writes:

I think at least a
* command_tag text

Added.

Why is there explicit documentation of not checking the arguments of said
trigger function? That seems to be a bit strange to me.

The code is searching for a function with the given name and 5 text
arguments, whatever you say in the command. That could be changed later
on if there's a need.

Forget it, i was too dumb to remember the code when I wrote the above ;)

schemaname currently is mightily unusable because whether its sent or not
depends wether the table was created with a schemaname specified or not.
That doesn't seem to be a good approach.

I'm not sure about that, we're spiting out what the user entered.

No, were not. Were writing out something that semantically the same what the
user entered. At several places NULL/NOT NULL and such get added.

As an alternative it would be possible to pass the current search path
but that doesn't seem to the best approach to me;

The trigger runs with the same search_path settings as the command, right?

For many things it makes sense to specify the search path of a function uppon
creation of the function. Even moreso if you have security definer
functions...
Otherwise you can get into troubles via operators and such...

Then there is nice stuff like CREATE TABLE .... (LIKE ...);
What shall that return in future? I vote for showing it as the plain
CREATE TABLE where all columns are specified.

I don't think so. Think about the main use cases for this patch,
auditing and replication. Both cases you want to deal with what the
user said, not what the system understood.

Is that so? In the replication case the origin table very well may look
differently on the master compared to the standby. Which is about impossible
to diagnose with the above behaviour.

I think it would sensible to allow multiple actions on which to trigger
to be specified just as you can for normal triggers. I also would like
an ALL option

Both are now implemented.

Cool, will check.

When dealing with an ANY command trigger, your procedure can get fired
on a command for which we don't have internal support for back parsing
etc, so you only get the command tag not null. I think that's the way to
go here, as I don't want to think we need to implement full support for
command triggers on ALTER OPERATOR FAMILY from the get go.

Thats ok with me.

Currently the grammar allows options to be passed to command triggers. Do
we want to keep that? If yes, with the same arcane set of datatypes as
in data triggers?
If yes it needs to be wired up.

In fact it's not the case, you always get called with the same 5
arguments, all nullable now that we have ANY COMMAND support.

I think you misunderstood me. Check the following excerpt from gram.y:
CreateCmdTrigStmt:
CREATE TRIGGER name TriggerActionTime COMMAND trigger_command_list
EXECUTE PROCEDURE func_name '(' TriggerFuncArgs ')'

You have TriggerfuncArgs there but you ignore them.

* schema qualification:
An option to add triggers only to a specific schema would be rather neat
for many scenarios.
I am not totally sure if its worth the hassle of defining what happens in
the edge cases of e.g. moving from one into another schema though.

I had written down support for a WHEN clause in command triggers, but
the exact design has yet to be worked out. I'd like to have this
facility but I'd like it even more if that could happen in a later
patch.

Hm. I am not sure a generic WHEN clause is needed. In my opinion schema
qualification would be enough...
A later patch is fine imo.

I contrast to data triggers command triggers cannot change what is done.
They can either prevent it from running or not. Which imo is fine.
The question I have is whether it would be a good idea to make it easy to
prevent recursion.... Especially if the triggers wont be per schema.

You already have

ATER TRIGGER foo ON COMMAND create table DISABLE;

that you can use from the trigger procedure itself. Of course, locking
is an issue if you want to go parallel on running commands with
recursive triggers attached. I think we might be able to skip solving
that problem in the first run.

hm. Not yet convinced. I need to think about it a bit.

Have fun ;)

Andres

#18Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#9)
Re: Command Triggers

On Friday, December 02, 2011 03:09:55 AM Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

On Thursday, December 01, 2011 07:21:25 PM Tom Lane wrote:

Making this work cleanly would be a bigger deal than I think you're
thinking.

Obviously that depends on the definition of clean...

Changing the grammar to make that explicit seems to involve a bit too
many changes on a first glance. The cheap way out seems to be to make
the decision in analyze.c:transformQuery.
Would that be an acceptable way forward?

ITYM transformStmt, but yeah, somewhere around there is probably
reasonable. The way I'm imagining this would work is that IntoClause
disappears from Query altogether: analyze.c would build a utility
statement
CreateTableAs, pull the IntoClause out of the SelectStmt structure and
put it into the utility statement, and then proceed much as we do for
ExplainStmt (and for the same reasons).

Ok. Because my book turned out to be boring I started looking at this. I
wonder if wouldn't be better to do check in directly in raw_parser(). The
advantage would be that that magically would fix the issue of not logging
CTAS/select when using log_statement = ddl and we don't need a snapshot or
such so that shouldn't be a problem.

Any arguments against doing so?

Andres

#19Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#9)
Re: Command Triggers

On Friday, December 02, 2011 03:09:55 AM Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

On Thursday, December 01, 2011 07:21:25 PM Tom Lane wrote:

Making this work cleanly would be a bigger deal than I think you're
thinking.

Obviously that depends on the definition of clean...

Changing the grammar to make that explicit seems to involve a bit too
many changes on a first glance. The cheap way out seems to be to make
the decision in analyze.c:transformQuery.
Would that be an acceptable way forward?

ITYM transformStmt, but yeah, somewhere around there is probably
reasonable. The way I'm imagining this would work is that IntoClause
disappears from Query altogether: analyze.c would build a utility
statement
CreateTableAs, pull the IntoClause out of the SelectStmt structure and
put it into the utility statement, and then proceed much as we do for
ExplainStmt (and for the same reasons).

I have a patch which basically does all this minus some cleanup. I currently
do the the differentiation for SELECT INTO (not CREATE AS, thats done in
gram.y) in raw_parser but thats quick to move if others don't aggree on that.

I have two questions now:

First, does anybody think it would be worth getting rid of the duplication
from OpenIntoRel (formerly from execMain.c) in regard to DefineRelation()?
I noticed that there already is some diversion between both. E.g. CREATE TABLE
frak TABLESPACE pg_global AS SELECT 1; is possible while it would be forbidden
via a plain CREATE TABLE. (I will send a fix for this separately).

Secondly, I am currently wondering whether it would be a good idea to use the
ModifyTable infrastructure for doing the insertion instead an own DestReceiver
infrastructure thats only used for CTAS.
It looks a bit too complicated to do this without removing the bulk insert and
HEAP_INSERT_SKIP_WAL optimizations.

Andres

#20Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#19)
Re: Command Triggers

Andres Freund <andres@anarazel.de> writes:

I have two questions now:

First, does anybody think it would be worth getting rid of the duplication
from OpenIntoRel (formerly from execMain.c) in regard to DefineRelation()?

That's probably reasonable to do, since as you say it would remove the
opportunity for bugs-of-omission in the CTAS table creation step.
OTOH, if you find yourself having to make any significant changes to
DefineRelation, then maybe not.

Secondly, I am currently wondering whether it would be a good idea to use the
ModifyTable infrastructure for doing the insertion instead an own DestReceiver
infrastructure thats only used for CTAS.

I think this is probably a bad idea; it will complicate matters and buy
little. There's not a lot of stuff needed for the actual data insertion
step, since we know the table can't have any defaults, constraints,
triggers, etc as yet.

regards, tom lane

#21Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#20)
1 attachment(s)
Re: Command Triggers

Hi,

Attached is a first version of the patch.

On Sunday, December 04, 2011 05:34:44 PM Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

I have two questions now:

First, does anybody think it would be worth getting rid of the
duplication from OpenIntoRel (formerly from execMain.c) in regard to
DefineRelation()?

That's probably reasonable to do, since as you say it would remove the
opportunity for bugs-of-omission in the CTAS table creation step.
OTOH, if you find yourself having to make any significant changes to
DefineRelation, then maybe not.

Building a CreateStmt seems to work well enough so far.

The only problem with that approach so far that I found is that:

CREATE TABLE collate_test2 (
a int,
b text COLLATE "POSIX"
);

CREATE TABLE collate_test1 (
a int,
b text COLLATE "C" NOT NULL
);

CREATE TABLE test_u AS SELECT a, b FROM collate_test1 UNION ALL SELECT a, b
FROM collate_test2; -- fail

failed with:
ERROR: no collation was derived for column "b" with collatable type text
HINT: Use the COLLATE clause to set the collation explicitly.

"works" now.

I am currently setting ColumnDef.collOid of new collumns to attcollation of
the QueryDesc's column. Unfortunately they have a different meaning...

Secondly, I am currently wondering whether it would be a good idea to use
the ModifyTable infrastructure for doing the insertion instead an own
DestReceiver infrastructure thats only used for CTAS.

I think this is probably a bad idea; it will complicate matters and buy
little. There's not a lot of stuff needed for the actual data insertion
step, since we know the table can't have any defaults, constraints,
triggers, etc as yet.

I got to the same conclusion.

Remaining problems are:
* how to tell ExecContextForcesOids which oid we want
* implementing CREATE TABLE AS ... EXECUTE without duplicating ExecuteQuery
* the attcollation setting problems from above
* returning better error messages for using INTO at places its not allowed

Comments about the direction of the patch?

Andres

Attachments:

0001-Transform-CREATE-TABLE-AS-SELECT-INTO-into-a-utility.patchtext/x-patch; charset=UTF-8; name=0001-Transform-CREATE-TABLE-AS-SELECT-INTO-into-a-utility.patchDownload
From 334a60a213804e32d94482211e7e2824b4253b35 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Sun, 4 Dec 2011 19:51:31 +0100
Subject: [PATCH] Transform CREATE TABLE AS/SELECT INTO into a utility
 statement

Currently missing parts are:
* CREATE TABLE AS ... EXECUTE
* oid handling
* difference in handling of collations
* some error messages are too generic
---
 src/backend/access/heap/heapam.c           |    9 +-
 src/backend/commands/copy.c                |    4 +-
 src/backend/commands/prepare.c             |   23 +--
 src/backend/commands/tablecmds.c           |  225 ++++++++++++++++
 src/backend/commands/view.c                |    3 +
 src/backend/executor/execMain.c            |  390 +---------------------------
 src/backend/executor/execUtils.c           |    2 -
 src/backend/executor/functions.c           |    4 +-
 src/backend/executor/spi.c                 |   11 +-
 src/backend/nodes/copyfuncs.c              |   18 +-
 src/backend/nodes/equalfuncs.c             |   16 +-
 src/backend/nodes/outfuncs.c               |    4 +-
 src/backend/nodes/readfuncs.c              |    1 -
 src/backend/optimizer/plan/planner.c       |    1 -
 src/backend/optimizer/plan/subselect.c     |    1 -
 src/backend/optimizer/prep/prepjointree.c  |    6 +-
 src/backend/optimizer/util/clauses.c       |    4 +-
 src/backend/parser/analyze.c               |   86 ++++--
 src/backend/parser/gram.y                  |   27 ++-
 src/backend/parser/parse_clause.c          |    6 -
 src/backend/parser/parse_cte.c             |    3 +
 src/backend/parser/parse_expr.c            |    4 +
 src/backend/parser/parser.c                |   38 +++
 src/backend/rewrite/rewriteDefine.c        |    3 +-
 src/backend/tcop/pquery.c                  |   13 +-
 src/backend/tcop/utility.c                 |   46 ++--
 src/include/access/htup.h                  |    2 +-
 src/include/commands/tablecmds.h           |    4 +
 src/include/nodes/execnodes.h              |    3 +
 src/include/nodes/nodes.h                  |    1 +
 src/include/nodes/parsenodes.h             |   20 +-
 src/include/nodes/plannodes.h              |    2 -
 src/pl/plpgsql/src/pl_exec.c               |   27 +--
 src/test/regress/expected/transactions.out |    2 +-
 34 files changed, 483 insertions(+), 526 deletions(-)

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 7b27c23..06d5d1d 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -2044,7 +2044,10 @@ heap_prepare_insert(Relation relation, HeapTuple tup, TransactionId xid,
 	else
 	{
 		/* check there is not space for an OID */
-		Assert(!(tup->t_data->t_infomask & HEAP_HASOID));
+		//FIXME: CTAS move
+		if(tup->t_data->t_infomask & HEAP_HASOID){
+			elog(ERROR, "Assert(!(tup->t_data->t_infomask & HEAP_HASOID))");
+		}
 	}
 
 	tup->t_data->t_infomask &= ~(HEAP_XACT_MASK);
@@ -2919,7 +2922,9 @@ l2:
 	else
 	{
 		/* check there is not space for an OID */
-		Assert(!(newtup->t_data->t_infomask & HEAP_HASOID));
+		if(newtup->t_data->t_infomask & HEAP_HASOID){
+			elog(ERROR, "Assert(!(newtup->t_data->t_infomask & HEAP_HASOID))");
+		}
 	}
 
 	newtup->t_data->t_infomask &= ~(HEAP_XACT_MASK);
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 9c994ef..09dbb8d 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -1211,12 +1211,14 @@ BeginCopy(bool is_from,
 		Assert(query->commandType == CMD_SELECT);
 		Assert(query->utilityStmt == NULL);
 
+#if 0
+//FIXME: improve error message at upper layer
 		/* Query mustn't use INTO, either */
 		if (query->intoClause)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("COPY (SELECT INTO) is not supported")));
-
+#endif
 		/* plan the query */
 		plan = planner(query, 0, NULL);
 
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index a949215..a423628 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -222,6 +222,8 @@ ExecuteQuery(ExecuteStmt *stmt, const char *queryString,
 	query_string = MemoryContextStrdup(PortalGetHeapMemory(portal),
 									   entry->plansource->query_string);
 
+#if 0
+	//FIXME: Reimplement this after CTAS move
 	/*
 	 * For CREATE TABLE / AS EXECUTE, we must make a copy of the stored query
 	 * so that we can modify its destination (yech, but this has always been
@@ -263,10 +265,11 @@ ExecuteQuery(ExecuteStmt *stmt, const char *queryString,
 	}
 	else
 	{
-		/* Replan if needed, and increment plan refcount for portal */
-		cplan = GetCachedPlan(entry->plansource, paramLI, false);
-		plan_list = cplan->stmt_list;
 	}
+#endif
+	/* Replan if needed, and increment plan refcount for portal */
+	cplan = GetCachedPlan(entry->plansource, paramLI, false);
+	plan_list = cplan->stmt_list;
 
 	PortalDefineQuery(portal,
 					  NULL,
@@ -666,20 +669,6 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainState *es,
 
 		if (IsA(pstmt, PlannedStmt))
 		{
-			if (execstmt->into)
-			{
-				if (pstmt->commandType != CMD_SELECT ||
-					pstmt->utilityStmt != NULL)
-					ereport(ERROR,
-							(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-							 errmsg("prepared statement is not a SELECT")));
-
-				/* Copy the stmt so we can modify it */
-				pstmt = copyObject(pstmt);
-
-				pstmt->intoClause = execstmt->into;
-			}
-
 			ExplainOnePlan(pstmt, es, query_string, paramLI);
 		}
 		else
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c4622c0..1d98e69 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -68,6 +68,7 @@
 #include "parser/parser.h"
 #include "rewrite/rewriteDefine.h"
 #include "rewrite/rewriteHandler.h"
+#include "tcop/tcopprot.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
 #include "storage/lock.h"
@@ -195,6 +196,18 @@ struct dropmsgstrings
 	const char *drophint_msg;
 };
 
+/*
+ * Support structure for CTAS
+ */
+typedef struct
+{
+	DestReceiver pub;			/* publicly-known function pointers */
+	EState	   *estate;			/* EState we are working with */
+	Relation	rel;			/* Relation to write to */
+	int			hi_options;		/* heap_insert performance options */
+	BulkInsertState bistate;	/* bulk insert state */
+} DR_intorel;
+
 static const struct dropmsgstrings dropmsgstringarray[] = {
 	{RELKIND_RELATION,
 		ERRCODE_UNDEFINED_TABLE,
@@ -640,6 +653,129 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId)
 	return relationId;
 }
 
+void
+CreateTableAs(CreateTableAsStmt *stmt, const char *queryString,
+              ParamListInfo params, DestReceiver *dest){
+	IntoClause *into = stmt->into;
+	Query      *query;
+	List	   *rewritten;
+	PlannedStmt *plan;
+	QueryDesc  *queryDesc;
+	Relation  intoRelationDesc;
+	CreateStmt* create;
+	int natt;
+	Oid intoRelationId;
+	ListCell* lc;
+
+	Assert(IsA(stmt->query, Query));
+	query = (Query*)stmt->query;
+
+	if(query->commandType == CMD_SELECT){
+	}
+	else if(query->commandType == CMD_UTILITY &&
+	        nodeTag(query->utilityStmt) == T_ExecuteStmt){
+		elog(ERROR, "CREATE TABLE ... AS EXECUTE is unimplemented right now");
+	}
+	else{
+		elog(ERROR, "unsupported command for CREATE TABLE AS");
+	}
+
+	rewritten = QueryRewrite((Query *) copyObject(stmt->query));
+	if(!rewritten)
+		elog(ERROR, "CREATE TABLE AS/SELECT INTO query rewrote to nothing");
+
+	if(list_length(rewritten) != 1)
+		elog(ERROR, "CREATE TABLE AS/SELECT INTO query rewrote to more than one query");
+
+	plan = pg_plan_query(lfirst(list_head(rewritten)), 0, params);
+
+	/*
+	 * Use a snapshot with an updated command ID to ensure this query sees
+	 * results of any previously executed queries.
+	 */
+	PushCopiedSnapshot(GetActiveSnapshot());
+	UpdateActiveSnapshotCommandId();
+
+	queryDesc = CreateQueryDesc(plan, queryString,
+								GetActiveSnapshot(), InvalidSnapshot,
+								CreateDestReceiver(DestIntoRel), params,
+	                            false);
+
+	/*
+	 * call ExecutorStart to prepare the plan for execution. Only
+	 * after this we have enough information to actually create a
+	 * target relation
+	 */
+	ExecutorStart(queryDesc, 0);
+
+
+	/*
+	 * create the target relation
+	 */
+	create = makeNode(CreateStmt);
+	create->relation = into->rel;
+	create->options = into->options;
+	create->oncommit = into->onCommit;
+	create->tablespacename = into->tableSpaceName;
+
+	if (list_length(into->colNames) > queryDesc->tupDesc->natts)
+		ereport(ERROR,
+		        (errcode(ERRCODE_SYNTAX_ERROR),
+		         errmsg("CREATE TABLE AS specifies too many column names")));
+	lc = list_head(into->colNames);
+	for(natt = 0; natt < queryDesc->tupDesc->natts; natt++){
+		ColumnDef *col = makeNode(ColumnDef);
+		TypeName *type = makeNode(TypeName);
+
+		Form_pg_attribute attribute = queryDesc->tupDesc->attrs[natt];
+
+		/*
+		 * If a column name list was specified in CREATE TABLE AS,
+		 * override the column names derived from the query.  (Too few
+		 * column names are OK, too many are not.)
+		 */
+		if(!lc)
+			col->colname = NameStr(attribute->attname);
+		else{
+			col->colname = strVal(lfirst(lc));
+			lc = lnext(lc);
+		}
+
+		col->collOid = attribute->attcollation;
+
+		type->typeOid = attribute->atttypid;
+		type->typemod = attribute->atttypmod;
+		col->typeName= type;
+		create->tableElts = lappend(create->tableElts, col);
+	}
+
+	//setup target
+	intoRelationId = DefineRelation(create, RELKIND_RELATION, InvalidOid);
+	intoRelationDesc = heap_open(intoRelationId, AccessExclusiveLock);
+
+	if(!into->skipData){
+		DR_intorel *myState = (DR_intorel *) queryDesc->dest;
+		/* setup the target of the DestIntoRel receiver */
+		myState->estate = queryDesc->estate;
+		myState->rel = intoRelationDesc;
+
+		/* run the plan */
+		ExecutorRun(queryDesc, ForwardScanDirection, 0L);
+
+	}
+
+	/* run cleanup too */
+	ExecutorFinish(queryDesc);
+	ExecutorEnd(queryDesc);
+
+	/* close rel, but keep lock until commit */
+	heap_close(intoRelationDesc, NoLock);
+
+	FreeQueryDesc(queryDesc);
+
+	PopActiveSnapshot();
+}
+
 /*
  * Emit the right error or warning message for a "DROP" command issued on a
  * non-existent relation
@@ -9771,3 +9907,92 @@ AtEOSubXact_on_commit_actions(bool isCommit, SubTransactionId mySubid,
 		}
 	}
 }
+
+/*
+ * Support for SELECT INTO (a/k/a CREATE TABLE AS)
+ *
+ * We implement SELECT INTO by diverting SELECT's normal output with
+ * a specialized DestReceiver type.
+ */
+
+/*
+ * intorel_startup --- executor startup
+ */
+static void
+intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	DR_intorel *myState = (DR_intorel *) self;
+	myState->bistate = GetBulkInsertState();
+}
+
+/*
+ * intorel_receive --- receive one tuple
+ */
+static void
+intorel_receive(TupleTableSlot *slot, DestReceiver *self)
+{
+	DR_intorel *myState = (DR_intorel *) self;
+	HeapTuple	tuple;
+
+	/*
+	 * get the heap tuple out of the tuple table slot, making sure we have a
+	 * writable copy
+	 */
+	tuple = ExecMaterializeSlot(slot);
+
+	/*
+	 * force assignment of new OID (see comments in ExecInsert)
+	 */
+	if (myState->rel->rd_rel->relhasoids)
+		HeapTupleSetOid(tuple, InvalidOid);
+
+	heap_insert(myState->rel,
+				tuple,
+				myState->estate->es_output_cid,
+				myState->hi_options,
+				myState->bistate);
+
+	/* We know this is a newly created relation, so there are no indexes */
+}
+
+/*
+ * intorel_shutdown --- executor end
+ */
+static void
+intorel_shutdown(DestReceiver *self)
+{
+	DR_intorel *myState = (DR_intorel *) self;
+
+	FreeBulkInsertState(myState->bistate);
+	if (myState->hi_options & HEAP_INSERT_SKIP_WAL)
+		heap_sync(myState->rel);
+}
+
+/*
+ * intorel_destroy --- release DestReceiver object
+ */
+static void
+intorel_destroy(DestReceiver *self)
+{
+	pfree(self);
+}
+
+/*
+ * CreateIntoRelDestReceiver -- create a suitable DestReceiver object
+ *
+ */
+DestReceiver *
+CreateIntoRelDestReceiver(void)
+{
+	DR_intorel *self = (DR_intorel *) palloc0(sizeof(DR_intorel));
+
+	self->pub.receiveSlot = intorel_receive;
+	self->pub.rStartup = intorel_startup;
+	self->pub.rShutdown = intorel_shutdown;
+	self->pub.rDestroy = intorel_destroy;
+	self->pub.mydest = DestIntoRel;
+
+	/* private fields will be set by OpenIntoRel */
+
+	return (DestReceiver *) self;
+}
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index b238199..594bcce 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -441,10 +441,13 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	 * DefineQueryRewrite(), but that function will complain about a bogus ON
 	 * SELECT rule, and we'd rather the message complain about a view.
 	 */
+#if 0
+//FIXME: better error message on toplevel
 	if (viewParse->intoClause != NULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("views must not contain SELECT INTO")));
+#endif
 	if (viewParse->hasModifyingCTE)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index d19e097..bc03ec5 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -87,12 +87,14 @@ static bool ExecCheckRTEPerms(RangeTblEntry *rte);
 static void ExecCheckXactReadOnly(PlannedStmt *plannedstmt);
 static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate,
 				  Plan *planTree);
+#if 0
 static void OpenIntoRel(QueryDesc *queryDesc);
 static void CloseIntoRel(QueryDesc *queryDesc);
 static void intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo);
 static void intorel_receive(TupleTableSlot *slot, DestReceiver *self);
 static void intorel_shutdown(DestReceiver *self);
 static void intorel_destroy(DestReceiver *self);
+#endif
 
 /* end of local decls */
 
@@ -171,11 +173,10 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 		case CMD_SELECT:
 
 			/*
-			 * SELECT INTO, SELECT FOR UPDATE/SHARE and modifying CTEs need to
+			 * SELECT FOR UPDATE/SHARE and modifying CTEs need to
 			 * mark tuples
 			 */
-			if (queryDesc->plannedstmt->intoClause != NULL ||
-				queryDesc->plannedstmt->rowMarks != NIL ||
+			if (queryDesc->plannedstmt->rowMarks != NIL ||
 				queryDesc->plannedstmt->hasModifyingCTE)
 				estate->es_output_cid = GetCurrentCommandId(true);
 
@@ -307,13 +308,6 @@ standard_ExecutorRun(QueryDesc *queryDesc,
 		(*dest->rStartup) (dest, operation, queryDesc->tupDesc);
 
 	/*
-	 * if it's CREATE TABLE AS ... WITH NO DATA, skip plan execution
-	 */
-	if (estate->es_select_into &&
-		queryDesc->plannedstmt->intoClause->skipData)
-		direction = NoMovementScanDirection;
-
-	/*
 	 * run plan
 	 */
 	if (!ScanDirectionIsNoMovement(direction))
@@ -448,12 +442,6 @@ standard_ExecutorEnd(QueryDesc *queryDesc)
 
 	ExecEndPlan(queryDesc->planstate, estate);
 
-	/*
-	 * Close the SELECT INTO relation if any
-	 */
-	if (estate->es_select_into)
-		CloseIntoRel(queryDesc);
-
 	/* do away with our snapshots */
 	UnregisterSnapshot(estate->es_snapshot);
 	UnregisterSnapshot(estate->es_crosscheck_snapshot);
@@ -703,15 +691,6 @@ ExecCheckXactReadOnly(PlannedStmt *plannedstmt)
 {
 	ListCell   *l;
 
-	/*
-	 * CREATE TABLE AS or SELECT INTO?
-	 *
-	 * XXX should we allow this if the destination is temp?  Considering that
-	 * it would still require catalog changes, probably not.
-	 */
-	if (plannedstmt->intoClause != NULL)
-		PreventCommandIfReadOnly(CreateCommandTag((Node *) plannedstmt));
-
 	/* Fail if write permissions are requested on any non-temp table */
 	foreach(l, plannedstmt->rtable)
 	{
@@ -861,18 +840,6 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 	}
 
 	/*
-	 * Detect whether we're doing SELECT INTO.  If so, set the es_into_oids
-	 * flag appropriately so that the plan tree will be initialized with the
-	 * correct tuple descriptors.  (Other SELECT INTO stuff comes later.)
-	 */
-	estate->es_select_into = false;
-	if (operation == CMD_SELECT && plannedstmt->intoClause != NULL)
-	{
-		estate->es_select_into = true;
-		estate->es_into_oids = interpretOidsOption(plannedstmt->intoClause->options);
-	}
-
-	/*
 	 * Initialize the executor's tuple table to empty.
 	 */
 	estate->es_tupleTable = NIL;
@@ -923,9 +890,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 	planstate = ExecInitNode(plan, estate, eflags);
 
 	/*
-	 * Get the tuple descriptor describing the type of tuples to return. (this
-	 * is especially important if we are creating a relation with "SELECT
-	 * INTO")
+	 * Get the tuple descriptor describing the type of tuples to return.
 	 */
 	tupType = ExecGetResultType(planstate);
 
@@ -965,16 +930,6 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 
 	queryDesc->tupDesc = tupType;
 	queryDesc->planstate = planstate;
-
-	/*
-	 * If doing SELECT INTO, initialize the "into" relation.  We must wait
-	 * till now so we have the "clean" result tuple type to create the new
-	 * table from.
-	 *
-	 * If EXPLAIN, skip creating the "into" relation.
-	 */
-	if (estate->es_select_into && !(eflags & EXEC_FLAG_EXPLAIN_ONLY))
-		OpenIntoRel(queryDesc);
 }
 
 /*
@@ -1227,7 +1182,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 /*
  *		ExecContextForcesOids
  *
- * This is pretty grotty: when doing INSERT, UPDATE, or SELECT INTO,
+ * This is pretty grotty: when doing INSERT or UPDATE
  * we need to ensure that result tuples have space for an OID iff they are
  * going to be stored into a relation that has OIDs.  In other contexts
  * we are free to choose whether to leave space for OIDs in result tuples
@@ -1252,7 +1207,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
  * the ModifyTable node, so ModifyTable has to set es_result_relation_info
  * while initializing each subplan.
  *
- * SELECT INTO is even uglier, because we don't have the INTO relation's
+ * FIXME:SELECT INTO is even uglier, because we don't have the INTO relation's
  * descriptor available when this code runs; we have to look aside at a
  * flag set by InitPlan().
  */
@@ -1271,13 +1226,17 @@ ExecContextForcesOids(PlanState *planstate, bool *hasoids)
 			return true;
 		}
 	}
-
+#if 0
+/* FIXME: We need to get to know whether our result relation needs
+ * oids. This currently the only place where the executor retains
+ * knowledge about SELECT INTO
+ */
 	if (planstate->state->es_select_into)
 	{
 		*hasoids = planstate->state->es_into_oids;
 		return true;
 	}
-
+#endif
 	return false;
 }
 
@@ -2224,8 +2183,7 @@ EvalPlanQualStart(EPQState *epqstate, EState *parentestate, Plan *planTree)
 	estate->es_rowMarks = parentestate->es_rowMarks;
 	estate->es_top_eflags = parentestate->es_top_eflags;
 	estate->es_instrument = parentestate->es_instrument;
-	estate->es_select_into = parentestate->es_select_into;
-	estate->es_into_oids = parentestate->es_into_oids;
+
 	/* es_auxmodifytables must NOT be copied */
 
 	/*
@@ -2367,323 +2325,3 @@ EvalPlanQualEnd(EPQState *epqstate)
 	epqstate->origslot = NULL;
 }
 
-
-/*
- * Support for SELECT INTO (a/k/a CREATE TABLE AS)
- *
- * We implement SELECT INTO by diverting SELECT's normal output with
- * a specialized DestReceiver type.
- */
-
-typedef struct
-{
-	DestReceiver pub;			/* publicly-known function pointers */
-	EState	   *estate;			/* EState we are working with */
-	Relation	rel;			/* Relation to write to */
-	int			hi_options;		/* heap_insert performance options */
-	BulkInsertState bistate;	/* bulk insert state */
-} DR_intorel;
-
-/*
- * OpenIntoRel --- actually create the SELECT INTO target relation
- *
- * This also replaces QueryDesc->dest with the special DestReceiver for
- * SELECT INTO.  We assume that the correct result tuple type has already
- * been placed in queryDesc->tupDesc.
- */
-static void
-OpenIntoRel(QueryDesc *queryDesc)
-{
-	IntoClause *into = queryDesc->plannedstmt->intoClause;
-	EState	   *estate = queryDesc->estate;
-	TupleDesc	intoTupDesc = queryDesc->tupDesc;
-	Relation	intoRelationDesc;
-	char	   *intoName;
-	Oid			namespaceId;
-	Oid			tablespaceId;
-	Datum		reloptions;
-	Oid			intoRelationId;
-	DR_intorel *myState;
-	RangeTblEntry  *rte;
-	AttrNumber		attnum;
-	static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
-
-	Assert(into);
-
-	/*
-	 * XXX This code needs to be kept in sync with DefineRelation(). Maybe we
-	 * should try to use that function instead.
-	 */
-
-	/*
-	 * Check consistency of arguments
-	 */
-	if (into->onCommit != ONCOMMIT_NOOP
-		&& into->rel->relpersistence != RELPERSISTENCE_TEMP)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("ON COMMIT can only be used on temporary tables")));
-
-	/*
-	 * If a column name list was specified in CREATE TABLE AS, override the
-	 * column names derived from the query.  (Too few column names are OK, too
-	 * many are not.)  It would probably be all right to scribble directly on
-	 * the query's result tupdesc, but let's be safe and make a copy.
-	 */
-	if (into->colNames)
-	{
-		ListCell   *lc;
-
-		intoTupDesc = CreateTupleDescCopy(intoTupDesc);
-		attnum = 1;
-		foreach(lc, into->colNames)
-		{
-			char	   *colname = strVal(lfirst(lc));
-
-			if (attnum > intoTupDesc->natts)
-				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("CREATE TABLE AS specifies too many column names")));
-			namestrcpy(&(intoTupDesc->attrs[attnum - 1]->attname), colname);
-			attnum++;
-		}
-	}
-
-	/*
-	 * Find namespace to create in, check its permissions
-	 */
-	intoName = into->rel->relname;
-	namespaceId = RangeVarGetAndCheckCreationNamespace(into->rel);
-	RangeVarAdjustRelationPersistence(into->rel, namespaceId);
-
-	/*
-	 * Security check: disallow creating temp tables from security-restricted
-	 * code.  This is needed because calling code might not expect untrusted
-	 * tables to appear in pg_temp at the front of its search path.
-	 */
-	if (into->rel->relpersistence == RELPERSISTENCE_TEMP
-		&& InSecurityRestrictedOperation())
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("cannot create temporary table within security-restricted operation")));
-
-	/*
-	 * Select tablespace to use.  If not specified, use default tablespace
-	 * (which may in turn default to database's default).
-	 */
-	if (into->tableSpaceName)
-	{
-		tablespaceId = get_tablespace_oid(into->tableSpaceName, false);
-	}
-	else
-	{
-		tablespaceId = GetDefaultTablespace(into->rel->relpersistence);
-		/* note InvalidOid is OK in this case */
-	}
-
-	/* Check permissions except when using the database's default space */
-	if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace)
-	{
-		AclResult	aclresult;
-
-		aclresult = pg_tablespace_aclcheck(tablespaceId, GetUserId(),
-										   ACL_CREATE);
-
-		if (aclresult != ACLCHECK_OK)
-			aclcheck_error(aclresult, ACL_KIND_TABLESPACE,
-						   get_tablespace_name(tablespaceId));
-	}
-
-	/* Parse and validate any reloptions */
-	reloptions = transformRelOptions((Datum) 0,
-									 into->options,
-									 NULL,
-									 validnsps,
-									 true,
-									 false);
-	(void) heap_reloptions(RELKIND_RELATION, reloptions, true);
-
-	/* Now we can actually create the new relation */
-	intoRelationId = heap_create_with_catalog(intoName,
-											  namespaceId,
-											  tablespaceId,
-											  InvalidOid,
-											  InvalidOid,
-											  InvalidOid,
-											  GetUserId(),
-											  intoTupDesc,
-											  NIL,
-											  RELKIND_RELATION,
-											  into->rel->relpersistence,
-											  false,
-											  false,
-											  true,
-											  0,
-											  into->onCommit,
-											  reloptions,
-											  true,
-											  allowSystemTableMods);
-	Assert(intoRelationId != InvalidOid);
-
-	/*
-	 * Advance command counter so that the newly-created relation's catalog
-	 * tuples will be visible to heap_open.
-	 */
-	CommandCounterIncrement();
-
-	/*
-	 * If necessary, create a TOAST table for the INTO relation. Note that
-	 * AlterTableCreateToastTable ends with CommandCounterIncrement(), so that
-	 * the TOAST table will be visible for insertion.
-	 */
-	reloptions = transformRelOptions((Datum) 0,
-									 into->options,
-									 "toast",
-									 validnsps,
-									 true,
-									 false);
-
-	(void) heap_reloptions(RELKIND_TOASTVALUE, reloptions, true);
-
-	AlterTableCreateToastTable(intoRelationId, reloptions);
-
-	/*
-	 * And open the constructed table for writing.
-	 */
-	intoRelationDesc = heap_open(intoRelationId, AccessExclusiveLock);
-
-	/*
-	 * Check INSERT permission on the constructed table.
-	 */
-	rte = makeNode(RangeTblEntry);
-	rte->rtekind = RTE_RELATION;
-	rte->relid = intoRelationId;
-	rte->relkind = RELKIND_RELATION;
-	rte->requiredPerms = ACL_INSERT;
-
-	for (attnum = 1; attnum <= intoTupDesc->natts; attnum++)
-		rte->modifiedCols = bms_add_member(rte->modifiedCols,
-				attnum - FirstLowInvalidHeapAttributeNumber);
-
-	ExecCheckRTPerms(list_make1(rte), true);
-
-	/*
-	 * Now replace the query's DestReceiver with one for SELECT INTO
-	 */
-	queryDesc->dest = CreateDestReceiver(DestIntoRel);
-	myState = (DR_intorel *) queryDesc->dest;
-	Assert(myState->pub.mydest == DestIntoRel);
-	myState->estate = estate;
-	myState->rel = intoRelationDesc;
-
-	/*
-	 * We can skip WAL-logging the insertions, unless PITR or streaming
-	 * replication is in use. We can skip the FSM in any case.
-	 */
-	myState->hi_options = HEAP_INSERT_SKIP_FSM |
-		(XLogIsNeeded() ? 0 : HEAP_INSERT_SKIP_WAL);
-	myState->bistate = GetBulkInsertState();
-
-	/* Not using WAL requires smgr_targblock be initially invalid */
-	Assert(RelationGetTargetBlock(intoRelationDesc) == InvalidBlockNumber);
-}
-
-/*
- * CloseIntoRel --- clean up SELECT INTO at ExecutorEnd time
- */
-static void
-CloseIntoRel(QueryDesc *queryDesc)
-{
-	DR_intorel *myState = (DR_intorel *) queryDesc->dest;
-
-	/* OpenIntoRel might never have gotten called */
-	if (myState && myState->pub.mydest == DestIntoRel && myState->rel)
-	{
-		FreeBulkInsertState(myState->bistate);
-
-		/* If we skipped using WAL, must heap_sync before commit */
-		if (myState->hi_options & HEAP_INSERT_SKIP_WAL)
-			heap_sync(myState->rel);
-
-		/* close rel, but keep lock until commit */
-		heap_close(myState->rel, NoLock);
-
-		myState->rel = NULL;
-	}
-}
-
-/*
- * CreateIntoRelDestReceiver -- create a suitable DestReceiver object
- */
-DestReceiver *
-CreateIntoRelDestReceiver(void)
-{
-	DR_intorel *self = (DR_intorel *) palloc0(sizeof(DR_intorel));
-
-	self->pub.receiveSlot = intorel_receive;
-	self->pub.rStartup = intorel_startup;
-	self->pub.rShutdown = intorel_shutdown;
-	self->pub.rDestroy = intorel_destroy;
-	self->pub.mydest = DestIntoRel;
-
-	/* private fields will be set by OpenIntoRel */
-
-	return (DestReceiver *) self;
-}
-
-/*
- * intorel_startup --- executor startup
- */
-static void
-intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
-{
-	/* no-op */
-}
-
-/*
- * intorel_receive --- receive one tuple
- */
-static void
-intorel_receive(TupleTableSlot *slot, DestReceiver *self)
-{
-	DR_intorel *myState = (DR_intorel *) self;
-	HeapTuple	tuple;
-
-	/*
-	 * get the heap tuple out of the tuple table slot, making sure we have a
-	 * writable copy
-	 */
-	tuple = ExecMaterializeSlot(slot);
-
-	/*
-	 * force assignment of new OID (see comments in ExecInsert)
-	 */
-	if (myState->rel->rd_rel->relhasoids)
-		HeapTupleSetOid(tuple, InvalidOid);
-
-	heap_insert(myState->rel,
-				tuple,
-				myState->estate->es_output_cid,
-				myState->hi_options,
-				myState->bistate);
-
-	/* We know this is a newly created relation, so there are no indexes */
-}
-
-/*
- * intorel_shutdown --- executor end
- */
-static void
-intorel_shutdown(DestReceiver *self)
-{
-	/* no-op */
-}
-
-/*
- * intorel_destroy --- release DestReceiver object
- */
-static void
-intorel_destroy(DestReceiver *self)
-{
-	pfree(self);
-}
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index 65591e2..acab607 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -137,8 +137,6 @@ CreateExecutorState(void)
 
 	estate->es_top_eflags = 0;
 	estate->es_instrument = 0;
-	estate->es_select_into = false;
-	estate->es_into_oids = false;
 	estate->es_finished = false;
 
 	estate->es_exprcontexts = NIL;
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 45ca5ec..30185f2 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -349,7 +349,6 @@ init_execution_state(List *queryTree_list,
 
 			if (ps->commandType == CMD_SELECT &&
 				ps->utilityStmt == NULL &&
-				ps->intoClause == NULL &&
 				!ps->hasModifyingCTE)
 				fcache->lazyEval = lasttages->lazyEval = true;
 		}
@@ -1307,8 +1306,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
 	 */
 	if (parse &&
 		parse->commandType == CMD_SELECT &&
-		parse->utilityStmt == NULL &&
-		parse->intoClause == NULL)
+		parse->utilityStmt == NULL)
 	{
 		tlist_ptr = &parse->targetList;
 		tlist = parse->targetList;
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 688279c..b3d113d 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -1423,6 +1423,7 @@ SPI_getargcount(SPIPlanPtr plan)
  * INSERT ... RETURNING, but not SELECT ... INTO). In essence,
  * the result indicates if the command can be used with SPI_cursor_open
  *
+ * FIXME: adjust comment
  * Parameters
  *	  plan: A plan previously prepared using SPI_prepare
  */
@@ -1921,7 +1922,11 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 				if (_SPI_current->tuptable)
 					_SPI_current->processed = _SPI_current->tuptable->alloced -
 						_SPI_current->tuptable->free;
-				res = SPI_OK_UTILITY;
+				if(nodeTag(stmt) == T_CreateTableAsStmt &&
+				   ((CreateTableAsStmt*)stmt)->is_select_into)
+					res = SPI_OK_SELINTO;
+				else
+					res = SPI_OK_UTILITY;
 			}
 
 			/*
@@ -2046,9 +2051,7 @@ _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, long tcount)
 	{
 		case CMD_SELECT:
 			Assert(queryDesc->plannedstmt->utilityStmt == NULL);
-			if (queryDesc->plannedstmt->intoClause)		/* select into table? */
-				res = SPI_OK_SELINTO;
-			else if (queryDesc->dest->mydest != DestSPI)
+			if (queryDesc->dest->mydest != DestSPI)
 			{
 				/* Don't return SPI_OK_SELECT if we're discarding result */
 				res = SPI_OK_UTILITY;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c70a5bd..d8e7eb3 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -85,7 +85,6 @@ _copyPlannedStmt(PlannedStmt *from)
 	COPY_NODE_FIELD(rtable);
 	COPY_NODE_FIELD(resultRelations);
 	COPY_NODE_FIELD(utilityStmt);
-	COPY_NODE_FIELD(intoClause);
 	COPY_NODE_FIELD(subplans);
 	COPY_BITMAPSET_FIELD(rewindPlanIDs);
 	COPY_NODE_FIELD(rowMarks);
@@ -2417,7 +2416,6 @@ _copyQuery(Query *from)
 	COPY_SCALAR_FIELD(canSetTag);
 	COPY_NODE_FIELD(utilityStmt);
 	COPY_SCALAR_FIELD(resultRelation);
-	COPY_NODE_FIELD(intoClause);
 	COPY_SCALAR_FIELD(hasAggs);
 	COPY_SCALAR_FIELD(hasWindowFuncs);
 	COPY_SCALAR_FIELD(hasSubLinks);
@@ -3201,6 +3199,18 @@ _copyExplainStmt(ExplainStmt *from)
 	return newnode;
 }
 
+static CreateTableAsStmt *
+_copyCreateTableAsStmt(CreateTableAsStmt *from)
+{
+	CreateTableAsStmt *newnode = makeNode(CreateTableAsStmt);
+
+	COPY_NODE_FIELD(query);
+	COPY_NODE_FIELD(into);
+	COPY_SCALAR_FIELD(is_select_into);
+
+	return newnode;
+}
+
 static CreateSeqStmt *
 _copyCreateSeqStmt(CreateSeqStmt *from)
 {
@@ -3608,7 +3618,6 @@ _copyExecuteStmt(ExecuteStmt *from)
 	ExecuteStmt *newnode = makeNode(ExecuteStmt);
 
 	COPY_STRING_FIELD(name);
-	COPY_NODE_FIELD(into);
 	COPY_NODE_FIELD(params);
 
 	return newnode;
@@ -4243,6 +4252,9 @@ copyObject(void *from)
 		case T_ExplainStmt:
 			retval = _copyExplainStmt(from);
 			break;
+		case T_CreateTableAsStmt:
+			retval = _copyCreateTableAsStmt(from);
+			break;
 		case T_CreateSeqStmt:
 			retval = _copyCreateSeqStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f490a7a..e090e38 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -900,7 +900,6 @@ _equalQuery(Query *a, Query *b)
 	COMPARE_SCALAR_FIELD(canSetTag);
 	COMPARE_NODE_FIELD(utilityStmt);
 	COMPARE_SCALAR_FIELD(resultRelation);
-	COMPARE_NODE_FIELD(intoClause);
 	COMPARE_SCALAR_FIELD(hasAggs);
 	COMPARE_SCALAR_FIELD(hasWindowFuncs);
 	COMPARE_SCALAR_FIELD(hasSubLinks);
@@ -1560,6 +1559,17 @@ _equalExplainStmt(ExplainStmt *a, ExplainStmt *b)
 	return true;
 }
 
+
+static bool
+_equalCreateTableAsStmt(CreateTableAsStmt *a, CreateTableAsStmt *b)
+{
+	COMPARE_NODE_FIELD(query);
+	COMPARE_NODE_FIELD(into);
+	COMPARE_SCALAR_FIELD(is_select_into);
+
+	return true;
+}
+
 static bool
 _equalCreateSeqStmt(CreateSeqStmt *a, CreateSeqStmt *b)
 {
@@ -1903,7 +1913,6 @@ static bool
 _equalExecuteStmt(ExecuteStmt *a, ExecuteStmt *b)
 {
 	COMPARE_STRING_FIELD(name);
-	COMPARE_NODE_FIELD(into);
 	COMPARE_NODE_FIELD(params);
 
 	return true;
@@ -2786,6 +2795,9 @@ equal(void *a, void *b)
 		case T_ExplainStmt:
 			retval = _equalExplainStmt(a, b);
 			break;
+		case T_CreateTableAsStmt:
+			retval = _equalCreateTableAsStmt(a, b);
+			break;
 		case T_CreateSeqStmt:
 			retval = _equalCreateSeqStmt(a, b);
 			break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 31af47f..997f5e2 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -248,7 +248,6 @@ _outPlannedStmt(StringInfo str, PlannedStmt *node)
 	WRITE_NODE_FIELD(rtable);
 	WRITE_NODE_FIELD(resultRelations);
 	WRITE_NODE_FIELD(utilityStmt);
-	WRITE_NODE_FIELD(intoClause);
 	WRITE_NODE_FIELD(subplans);
 	WRITE_BITMAPSET_FIELD(rewindPlanIDs);
 	WRITE_NODE_FIELD(rowMarks);
@@ -2197,7 +2196,6 @@ _outQuery(StringInfo str, Query *node)
 		appendStringInfo(str, " :utilityStmt <>");
 
 	WRITE_INT_FIELD(resultRelation);
-	WRITE_NODE_FIELD(intoClause);
 	WRITE_BOOL_FIELD(hasAggs);
 	WRITE_BOOL_FIELD(hasWindowFuncs);
 	WRITE_BOOL_FIELD(hasSubLinks);
@@ -2813,7 +2811,7 @@ _outNode(StringInfo str, void *obj)
 			case T_RangeVar:
 				_outRangeVar(str, obj);
 				break;
-			case T_IntoClause:
+			case T_IntoClause:/*XXX: This could be removed but dim would need to add it right back*/
 				_outIntoClause(str, obj);
 				break;
 			case T_Var:
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3de20ad..8a35c4c 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -198,7 +198,6 @@ _readQuery(void)
 	READ_BOOL_FIELD(canSetTag);
 	READ_NODE_FIELD(utilityStmt);
 	READ_INT_FIELD(resultRelation);
-	READ_NODE_FIELD(intoClause);
 	READ_BOOL_FIELD(hasAggs);
 	READ_BOOL_FIELD(hasWindowFuncs);
 	READ_BOOL_FIELD(hasSubLinks);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 5c18b72..93b7773 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -233,7 +233,6 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	result->rtable = glob->finalrtable;
 	result->resultRelations = glob->resultRelations;
 	result->utilityStmt = parse->utilityStmt;
-	result->intoClause = parse->intoClause;
 	result->subplans = glob->subplans;
 	result->rewindPlanIDs = glob->rewindPlanIDs;
 	result->rowMarks = glob->finalrowmarks;
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index e396520..a995fdf 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1399,7 +1399,6 @@ simplify_EXISTS_query(Query *query)
 	 * are complex.
 	 */
 	if (query->commandType != CMD_SELECT ||
-		query->intoClause ||
 		query->setOperations ||
 		query->hasAggs ||
 		query->hasWindowFuncs ||
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 8bb011b..f71a988 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1047,8 +1047,7 @@ is_simple_subquery(Query *subquery)
 	 */
 	if (!IsA(subquery, Query) ||
 		subquery->commandType != CMD_SELECT ||
-		subquery->utilityStmt != NULL ||
-		subquery->intoClause != NULL)
+		subquery->utilityStmt != NULL)
 		elog(ERROR, "subquery is bogus");
 
 	/*
@@ -1134,8 +1133,7 @@ is_simple_union_all(Query *subquery)
 	/* Let's just make sure it's a valid subselect ... */
 	if (!IsA(subquery, Query) ||
 		subquery->commandType != CMD_SELECT ||
-		subquery->utilityStmt != NULL ||
-		subquery->intoClause != NULL)
+		subquery->utilityStmt != NULL)
 		elog(ERROR, "subquery is bogus");
 
 	/* Is it a set-operation query at all? */
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index ad02950..7422387 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4028,7 +4028,6 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 	if (!IsA(querytree, Query) ||
 		querytree->commandType != CMD_SELECT ||
 		querytree->utilityStmt ||
-		querytree->intoClause ||
 		querytree->hasAggs ||
 		querytree->hasWindowFuncs ||
 		querytree->hasSubLinks ||
@@ -4542,8 +4541,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
 	 */
 	if (!IsA(querytree, Query) ||
 		querytree->commandType != CMD_SELECT ||
-		querytree->utilityStmt ||
-		querytree->intoClause)
+		querytree->utilityStmt)
 		goto fail;
 
 	/*
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index dae5478..10be4c9 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -62,6 +62,8 @@ static Query *transformDeclareCursorStmt(ParseState *pstate,
 						   DeclareCursorStmt *stmt);
 static Query *transformExplainStmt(ParseState *pstate,
 					 ExplainStmt *stmt);
+static Query *transformCreateTableAsStmt(ParseState *pstate,
+					 CreateTableAsStmt *stmt);
 static void transformLockingClause(ParseState *pstate, Query *qry,
 					   LockingClause *lc, bool pushedDown);
 
@@ -202,6 +204,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 										  (ExplainStmt *) parseTree);
 			break;
 
+		case T_CreateTableAsStmt:
+			result = transformCreateTableAsStmt(pstate,
+			                           (CreateTableAsStmt *) parseTree);
+			break;
+
 		default:
 
 			/*
@@ -459,17 +466,11 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
 
 		free_parsestate(sub_pstate);
 
-		/* The grammar should have produced a SELECT, but it might have INTO */
+		/* FIXME: comment: The grammar should have produced a SELECT, but it might have INTO */
 		if (!IsA(selectQuery, Query) ||
 			selectQuery->commandType != CMD_SELECT ||
 			selectQuery->utilityStmt != NULL)
 			elog(ERROR, "unexpected non-SELECT command in INSERT ... SELECT");
-		if (selectQuery->intoClause)
-			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("INSERT ... SELECT cannot specify INTO"),
-					 parser_errposition(pstate,
-						   exprLocation((Node *) selectQuery->intoClause))));
 
 		/*
 		 * Make the source be a subquery in the INSERT's rangetable, and add
@@ -876,6 +877,18 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	Node	   *qual;
 	ListCell   *l;
 
+	/*
+	 * Validity-check whether we got called from somewhere where
+	 * ... INTO was not allowed
+	 */
+	if (stmt->intoClause)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("INTO is only allowed on first SELECT of UNION/INTERSECT/EXCEPT"),
+				 parser_errposition(pstate,
+				                    exprLocation((Node *) stmt->intoClause))));
+
+
 	qry->commandType = CMD_SELECT;
 
 	/* process the WITH clause independently of all else */
@@ -963,8 +976,6 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 												   pstate->p_windowdefs,
 												   &qry->targetList);
 
-	/* SELECT INTO/CREATE TABLE AS spec is just passed through */
-	qry->intoClause = stmt->intoClause;
 
 	qry->rtable = pstate->p_rtable;
 	qry->jointree = makeFromExpr(pstate->p_joinlist, qual);
@@ -1021,6 +1032,17 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	Assert(stmt->windowClause == NIL);
 	Assert(stmt->op == SETOP_NONE);
 
+	/*
+	 * Validity-check whether we got called from somewhere where
+	 * ... INTO was not allowed
+	 */
+	if (stmt->intoClause)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("INTO is only allowed on first SELECT of UNION/INTERSECT/EXCEPT"),
+				 parser_errposition(pstate,
+				                    exprLocation((Node *) stmt->intoClause))));
+
 	/* process the WITH clause independently of all else */
 	if (stmt->withClause)
 	{
@@ -1185,9 +1207,6 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 			 errmsg("SELECT FOR UPDATE/SHARE cannot be applied to VALUES")));
 
-	/* CREATE TABLE AS spec is just passed through */
-	qry->intoClause = stmt->intoClause;
-
 	/*
 	 * There mustn't have been any table references in the expressions, else
 	 * strange things would happen, like Cartesian products of those tables
@@ -1253,7 +1272,6 @@ static Query *
 transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
-	SelectStmt *leftmostSelect;
 	int			leftmostRTI;
 	Query	   *leftmostQuery;
 	SetOperationStmt *sostmt;
@@ -1286,20 +1304,6 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 	}
 
 	/*
-	 * Find leftmost leaf SelectStmt; extract the one-time-only items from it
-	 * and from the top-level node.
-	 */
-	leftmostSelect = stmt->larg;
-	while (leftmostSelect && leftmostSelect->op != SETOP_NONE)
-		leftmostSelect = leftmostSelect->larg;
-	Assert(leftmostSelect && IsA(leftmostSelect, SelectStmt) &&
-		   leftmostSelect->larg == NULL);
-	qry->intoClause = leftmostSelect->intoClause;
-
-	/* clear this to prevent complaints in transformSetOperationTree() */
-	leftmostSelect->intoClause = NULL;
-
-	/*
 	 * These are not one-time, exactly, but we want to process them here and
 	 * not let transformSetOperationTree() see them --- else it'll just
 	 * recurse right back here!
@@ -1330,7 +1334,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 	qry->setOperations = (Node *) sostmt;
 
 	/*
-	 * Re-find leftmost SELECT (now it's a sub-query in rangetable)
+	 * Find leftmost SELECT (it's a sub-query in rangetable)
 	 */
 	node = sostmt->larg;
 	while (node && IsA(node, SetOperationStmt))
@@ -2107,6 +2111,8 @@ transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt)
 		result->utilityStmt != NULL)
 		elog(ERROR, "unexpected non-SELECT command in DECLARE CURSOR");
 
+#if 0
+FIXME
 	/* But we must explicitly disallow DECLARE CURSOR ... SELECT INTO */
 	if (result->intoClause)
 		ereport(ERROR,
@@ -2114,7 +2120,7 @@ transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt)
 				 errmsg("DECLARE CURSOR cannot specify INTO"),
 				 parser_errposition(pstate,
 								exprLocation((Node *) result->intoClause))));
-
+#endif
 	/*
 	 * We also disallow data-modifying WITH in a cursor.  (This could be
 	 * allowed, but the semantics of when the updates occur might be
@@ -2183,6 +2189,28 @@ transformExplainStmt(ParseState *pstate, ExplainStmt *stmt)
 
 
 /*
+ * transformCreateTableAsStmt -
+ *	transform an CREATE TABLE AS/SELECT ... INTO Statement
+ *
+ */
+static Query *
+transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt)
+{
+	Query	   *result;
+
+	/* transform contained query */
+	stmt->query = (Node *) transformStmt(pstate, stmt->query);
+
+	/* represent the command as a utility Query */
+	result = makeNode(Query);
+	result->commandType = CMD_UTILITY;
+	result->utilityStmt = (Node *) stmt;
+
+	return result;
+}
+
+
+/*
  * Check for features that are not supported together with FOR UPDATE/SHARE.
  *
  * exported so planner can check again after rewriting, query pullup, etc
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2a497d1..670832e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -3010,23 +3010,37 @@ ExistingIndex:   USING INDEX index_name				{ $$ = $3; }
 CreateAsStmt:
 		CREATE OptTemp TABLE create_as_target AS SelectStmt opt_with_data
 				{
+                    SelectStmt *n;
+					CreateTableAsStmt* ctas = makeNode(CreateTableAsStmt);
+                    ctas->into = $4;
+                    ctas->query = $6;
+                    ctas->is_select_into = false;
+                    $4->skipData = !($7);
+					$4->rel->relpersistence = $2;
+					$4->skipData = !($7);
+                    $$ = (Node*)ctas;
+
 					/*
 					 * When the SelectStmt is a set-operation tree, we must
 					 * stuff the INTO information into the leftmost component
 					 * Select, because that's where analyze.c will expect
 					 * to find it.
 					 */
-					SelectStmt *n = findLeftmostSelect((SelectStmt *) $6);
+					n = findLeftmostSelect((SelectStmt *) $6);
 					if (n->intoClause != NULL)
 						ereport(ERROR,
 								(errcode(ERRCODE_SYNTAX_ERROR),
 								 errmsg("CREATE TABLE AS cannot specify INTO"),
 								 parser_errposition(exprLocation((Node *) n->intoClause))));
+#if 0
+//FIXME
+
 					n->intoClause = $4;
 					/* cram additional flags into the IntoClause */
 					$4->rel->relpersistence = $2;
 					$4->skipData = !($7);
 					$$ = $6;
+#endif
 				}
 		;
 
@@ -7994,20 +8008,25 @@ ExecuteStmt: EXECUTE name execute_param_clause
 					ExecuteStmt *n = makeNode(ExecuteStmt);
 					n->name = $2;
 					n->params = $3;
-					n->into = NULL;
 					$$ = (Node *) n;
 				}
 			| CREATE OptTemp TABLE create_as_target AS
 				EXECUTE name execute_param_clause opt_with_data
 				{
 					ExecuteStmt *n = makeNode(ExecuteStmt);
+					CreateTableAsStmt* ctas = makeNode(CreateTableAsStmt);
+
 					n->name = $7;
 					n->params = $8;
-					n->into = $4;
+
 					/* cram additional flags into the IntoClause */
 					$4->rel->relpersistence = $2;
 					$4->skipData = !($9);
-					$$ = (Node *) n;
+
+                    ctas->into = $4;
+                    ctas->query = (Node*)n;
+                    ctas->is_select_into = false;
+					$$ = (Node *) ctas;
 				}
 		;
 
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index e8177bc..f834c43 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -495,12 +495,6 @@ transformRangeSubselect(ParseState *pstate, RangeSubselect *r)
 		query->commandType != CMD_SELECT ||
 		query->utilityStmt != NULL)
 		elog(ERROR, "unexpected non-SELECT command in subquery in FROM");
-	if (query->intoClause)
-		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("subquery in FROM cannot have SELECT INTO"),
-				 parser_errposition(pstate,
-								 exprLocation((Node *) query->intoClause))));
 
 	/*
 	 * The subquery cannot make use of any variables from FROM items created
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index ec6afd8..e2adf0b 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -253,12 +253,15 @@ analyzeCTE(ParseState *pstate, CommonTableExpr *cte)
 	if (query->utilityStmt != NULL)
 		elog(ERROR, "unexpected utility statement in WITH");
 
+#if 0
+//FIXME: recheck that this cannot happen
 	if (query->intoClause)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("subquery in WITH cannot have SELECT INTO"),
 				 parser_errposition(pstate,
 								 exprLocation((Node *) query->intoClause))));
+#endif
 
 	/*
 	 * We disallow data-modifying WITH except at the top level of a query,
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 75236c7..5597961 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1408,12 +1408,16 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		qtree->commandType != CMD_SELECT ||
 		qtree->utilityStmt != NULL)
 		elog(ERROR, "unexpected non-SELECT command in SubLink");
+
+#if 0
+	//FIXME: I don't think we can get here in a problematic case, recheck
 	if (qtree->intoClause)
 		ereport(ERROR,
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("subquery cannot have SELECT INTO"),
 				 parser_errposition(pstate,
 								 exprLocation((Node *) qtree->intoClause))));
+#endif
 
 	sublink->subselect = (Node *) qtree;
 
diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c
index e389208..de28d84 100644
--- a/src/backend/parser/parser.c
+++ b/src/backend/parser/parser.c
@@ -37,6 +37,7 @@ raw_parser(const char *str)
 	core_yyscan_t yyscanner;
 	base_yy_extra_type yyextra;
 	int			yyresult;
+	ListCell *c;
 
 	/* initialize the flex scanner */
 	yyscanner = scanner_init(str, &yyextra.core_yy_extra,
@@ -57,6 +58,43 @@ raw_parser(const char *str)
 	if (yyresult)				/* error */
 		return NIL;
 
+	/*
+	 * Some things are rather hard to properly diagnose in grammar
+	 * without complicating/duplicating it too much. So we do some
+	 * postprocessing here.
+	 */
+	foreach(c, yyextra.parsetree){
+		switch(nodeTag(lfirst(c))){
+			/*
+			 * The grammar currently doesn't disambiguate between
+			 * SELECT and SELECT ... INTO. Do that now.
+			 */
+			case T_SelectStmt:
+			{
+				SelectStmt* s = (SelectStmt*)lfirst(c);
+				SelectStmt* first = s;
+				while (first && first->op != SETOP_NONE)
+					first = first->larg;
+				Assert(first && IsA(first, SelectStmt) && first->larg == NULL);
+				if(first->intoClause){
+					CreateTableAsStmt* ctas = makeNode(CreateTableAsStmt);
+					ctas->into = first->intoClause;
+					ctas->query = (Node*)s;
+					ctas->is_select_into = true;
+					/*
+					 * this way everyone can complain if this is set
+					 * without further checks because it shall never
+					 * be set but here.
+					 */
+					first->intoClause = NULL;
+					lfirst(c) = ctas;
+					break;
+				}
+			}
+			default:
+				break;
+		}
+	}
 	return yyextra.parsetree;
 }
 
diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c
index 17db70e..e359e9a 100644
--- a/src/backend/rewrite/rewriteDefine.c
+++ b/src/backend/rewrite/rewriteDefine.c
@@ -324,8 +324,7 @@ DefineQueryRewrite(char *rulename,
 		query = (Query *) linitial(action);
 		if (!is_instead ||
 			query->commandType != CMD_SELECT ||
-			query->utilityStmt != NULL ||
-			query->intoClause != NULL)
+			query->utilityStmt != NULL)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("rules on SELECT must have action INSTEAD SELECT")));
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 466727b..2628e31 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -260,8 +260,7 @@ ChoosePortalStrategy(List *stmts)
 			if (query->canSetTag)
 			{
 				if (query->commandType == CMD_SELECT &&
-					query->utilityStmt == NULL &&
-					query->intoClause == NULL)
+					query->utilityStmt == NULL)
 				{
 					if (query->hasModifyingCTE)
 						return PORTAL_ONE_MOD_WITH;
@@ -285,8 +284,7 @@ ChoosePortalStrategy(List *stmts)
 			if (pstmt->canSetTag)
 			{
 				if (pstmt->commandType == CMD_SELECT &&
-					pstmt->utilityStmt == NULL &&
-					pstmt->intoClause == NULL)
+					pstmt->utilityStmt == NULL)
 				{
 					if (pstmt->hasModifyingCTE)
 						return PORTAL_ONE_MOD_WITH;
@@ -395,8 +393,7 @@ FetchStatementTargetList(Node *stmt)
 		else
 		{
 			if (query->commandType == CMD_SELECT &&
-				query->utilityStmt == NULL &&
-				query->intoClause == NULL)
+				query->utilityStmt == NULL)
 				return query->targetList;
 			if (query->returningList)
 				return query->returningList;
@@ -408,8 +405,7 @@ FetchStatementTargetList(Node *stmt)
 		PlannedStmt *pstmt = (PlannedStmt *) stmt;
 
 		if (pstmt->commandType == CMD_SELECT &&
-			pstmt->utilityStmt == NULL &&
-			pstmt->intoClause == NULL)
+			pstmt->utilityStmt == NULL)
 			return pstmt->planTree->targetlist;
 		if (pstmt->hasReturning)
 			return pstmt->planTree->targetlist;
@@ -430,7 +426,6 @@ FetchStatementTargetList(Node *stmt)
 		ExecuteStmt *estmt = (ExecuteStmt *) stmt;
 		PreparedStatement *entry;
 
-		Assert(!estmt->into);
 		entry = FetchPreparedStatement(estmt->name, true);
 		return FetchPreparedStatementTargetList(entry);
 	}
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 6f88c47..4c5be0a 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -127,9 +127,7 @@ CommandIsReadOnly(Node *parsetree)
 		switch (stmt->commandType)
 		{
 			case CMD_SELECT:
-				if (stmt->intoClause != NULL)
-					return false;		/* SELECT INTO */
-				else if (stmt->rowMarks != NIL)
+				if (stmt->rowMarks != NIL)
 					return false;		/* SELECT FOR UPDATE/SHARE */
 				else if (stmt->hasModifyingCTE)
 					return false;		/* data-modifying CTE */
@@ -198,6 +196,7 @@ check_xact_readonly(Node *parsetree)
 		case T_CreateSchemaStmt:
 		case T_CreateSeqStmt:
 		case T_CreateStmt:
+		case T_CreateTableAsStmt:
 		case T_CreateTableSpaceStmt:
 		case T_CreateTrigStmt:
 		case T_CompositeTypeStmt:
@@ -1017,6 +1016,10 @@ standard_ProcessUtility(Node *parsetree,
 			ExplainQuery((ExplainStmt *) parsetree, queryString, params, dest);
 			break;
 
+		case T_CreateTableAsStmt:
+			CreateTableAs((CreateTableAsStmt *) parsetree, queryString, params, dest);
+			break;
+
 		case T_VariableSetStmt:
 			ExecSetVariableStmt((VariableSetStmt *) parsetree);
 			break;
@@ -1211,8 +1214,6 @@ UtilityReturnsTuples(Node *parsetree)
 				ExecuteStmt *stmt = (ExecuteStmt *) parsetree;
 				PreparedStatement *entry;
 
-				if (stmt->into)
-					return false;
 				entry = FetchPreparedStatement(stmt->name, false);
 				if (!entry)
 					return false;		/* not our business to raise error */
@@ -1263,8 +1264,6 @@ UtilityTupleDescriptor(Node *parsetree)
 				ExecuteStmt *stmt = (ExecuteStmt *) parsetree;
 				PreparedStatement *entry;
 
-				if (stmt->into)
-					return NULL;
 				entry = FetchPreparedStatement(stmt->name, false);
 				if (!entry)
 					return NULL;	/* not our business to raise error */
@@ -1299,8 +1298,7 @@ QueryReturnsTuples(Query *parsetree)
 	{
 		case CMD_SELECT:
 			/* returns tuples ... unless it's DECLARE CURSOR or SELECT INTO */
-			if (parsetree->utilityStmt == NULL &&
-				parsetree->intoClause == NULL)
+			if (parsetree->utilityStmt == NULL)
 				return true;
 			break;
 		case CMD_INSERT:
@@ -1888,6 +1886,13 @@ CreateCommandTag(Node *parsetree)
 			tag = "EXPLAIN";
 			break;
 
+		case T_CreateTableAsStmt:
+			if (((CreateTableAsStmt*)parsetree)->is_select_into)
+				tag = "SELECT INTO";
+			else
+				tag = "CREATE TABLE AS";
+			break;
+
 		case T_VariableSetStmt:
 			switch (((VariableSetStmt *) parsetree)->kind)
 			{
@@ -2041,8 +2046,6 @@ CreateCommandTag(Node *parsetree)
 							Assert(IsA(stmt->utilityStmt, DeclareCursorStmt));
 							tag = "DECLARE CURSOR";
 						}
-						else if (stmt->intoClause != NULL)
-							tag = "SELECT INTO";
 						else if (stmt->rowMarks != NIL)
 						{
 							/* not 100% but probably close enough */
@@ -2091,8 +2094,6 @@ CreateCommandTag(Node *parsetree)
 							Assert(IsA(stmt->utilityStmt, DeclareCursorStmt));
 							tag = "DECLARE CURSOR";
 						}
-						else if (stmt->intoClause != NULL)
-							tag = "SELECT INTO";
 						else if (stmt->rowMarks != NIL)
 						{
 							/* not 100% but probably close enough */
@@ -2159,10 +2160,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_SelectStmt:
-			if (((SelectStmt *) parsetree)->intoClause)
-				lev = LOGSTMT_DDL;		/* CREATE AS, SELECT INTO */
-			else
-				lev = LOGSTMT_ALL;
+			lev = LOGSTMT_ALL;
 			break;
 
 			/* utility statements --- same whether raw or cooked */
@@ -2410,6 +2408,10 @@ GetCommandLogLevel(Node *parsetree)
 			}
 			break;
 
+		case T_CreateTableAsStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		case T_VariableSetStmt:
 			lev = LOGSTMT_ALL;
 			break;
@@ -2510,10 +2512,7 @@ GetCommandLogLevel(Node *parsetree)
 				switch (stmt->commandType)
 				{
 					case CMD_SELECT:
-						if (stmt->intoClause != NULL)
-							lev = LOGSTMT_DDL;	/* CREATE AS, SELECT INTO */
-						else
-							lev = LOGSTMT_ALL;	/* SELECT or DECLARE CURSOR */
+						lev = LOGSTMT_ALL;
 						break;
 
 					case CMD_UPDATE:
@@ -2539,10 +2538,7 @@ GetCommandLogLevel(Node *parsetree)
 				switch (stmt->commandType)
 				{
 					case CMD_SELECT:
-						if (stmt->intoClause != NULL)
-							lev = LOGSTMT_DDL;	/* CREATE AS, SELECT INTO */
-						else
-							lev = LOGSTMT_ALL;	/* SELECT or DECLARE CURSOR */
+						lev = LOGSTMT_ALL;
 						break;
 
 					case CMD_UPDATE:
diff --git a/src/include/access/htup.h b/src/include/access/htup.h
index 3ca25ac..01e3049 100644
--- a/src/include/access/htup.h
+++ b/src/include/access/htup.h
@@ -311,7 +311,7 @@ do { \
 
 #define HeapTupleHeaderSetOid(tup, oid) \
 do { \
-	Assert((tup)->t_infomask & HEAP_HASOID); \
+	if(!((tup)->t_infomask & HEAP_HASOID)) elog(ERROR, "HEAP_HASOID"); \
 	*((Oid *) ((char *)(tup) + (tup)->t_hoff - sizeof(Oid))) = (oid); \
 } while (0)
 
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 333e303..bac53ac 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -15,6 +15,7 @@
 #define TABLECMDS_H
 
 #include "access/htup.h"
+#include "executor/executor.h"
 #include "nodes/parsenodes.h"
 #include "storage/lock.h"
 #include "utils/relcache.h"
@@ -22,6 +23,9 @@
 
 extern Oid	DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId);
 
+extern void CreateTableAs(CreateTableAsStmt *stmt, const char *queryString,
+			 ParamListInfo params, DestReceiver *dest);
+
 extern void RemoveRelations(DropStmt *drop);
 
 extern void AlterTable(AlterTableStmt *stmt);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0a89f18..7d64f6c 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -370,8 +370,11 @@ typedef struct EState
 
 	int			es_top_eflags;	/* eflags passed to ExecutorStart */
 	int			es_instrument;	/* OR of InstrumentOption flags */
+#if 0
+	//FIXME: remove. We might need the oid part...
 	bool		es_select_into; /* true if doing SELECT INTO */
 	bool		es_into_oids;	/* true to generate OIDs in SELECT INTO */
+#endif
 	bool		es_finished;	/* true when ExecutorFinish is done */
 
 	List	   *es_exprcontexts;	/* List of ExprContexts within EState */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 3a24089..3f48b3f 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -305,6 +305,7 @@ typedef enum NodeTag
 	T_DropdbStmt,
 	T_VacuumStmt,
 	T_ExplainStmt,
+	T_CreateTableAsStmt,
 	T_CreateSeqStmt,
 	T_AlterSeqStmt,
 	T_VariableSetStmt,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9e277c5..6577660 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -111,8 +111,6 @@ typedef struct Query
 	int			resultRelation; /* rtable index of target relation for
 								 * INSERT/UPDATE/DELETE; 0 for SELECT */
 
-	IntoClause *intoClause;		/* target for SELECT INTO / CREATE TABLE AS */
-
 	bool		hasAggs;		/* has aggregates in tlist or havingQual */
 	bool		hasWindowFuncs; /* has window functions in tlist */
 	bool		hasSubLinks;	/* has subquery SubLink */
@@ -1008,7 +1006,7 @@ typedef struct SelectStmt
 	 */
 	List	   *distinctClause; /* NULL, list of DISTINCT ON exprs, or
 								 * lcons(NIL,NIL) for all (SELECT DISTINCT) */
-	IntoClause *intoClause;		/* target for SELECT INTO / CREATE TABLE AS */
+	IntoClause *intoClause;		/* target for SELECT INTO */
 	List	   *targetList;		/* the target list (of ResTarget) */
 	List	   *fromClause;		/* the FROM clause */
 	Node	   *whereClause;	/* WHERE qualification */
@@ -2373,7 +2371,7 @@ typedef struct VacuumStmt
  *		Explain Statement
  *
  * The "query" field is either a raw parse tree (SelectStmt, InsertStmt, etc)
- * or a Query node if parse analysis has been done.  Note that rewriting and
+ * or a Query node if parse analysis has been done. Note that rewriting and
  * planning of the query are always postponed until execution of EXPLAIN.
  * ----------------------
  */
@@ -2385,6 +2383,19 @@ typedef struct ExplainStmt
 } ExplainStmt;
 
 /* ----------------------
+ * analyzing, rewriting and planning are handled as in explain
+ * statements (see comment above) only that query can only be a
+ * SelectStmt and not some other type.
+ */
+typedef struct CreateTableAsStmt
+{
+	NodeTag		type;
+	IntoClause  *into;
+	Node        *query;			/* the query (see comments above) */
+	bool        is_select_into; /* plpgsql wants to disambiguate */
+} CreateTableAsStmt;
+
+/* ----------------------
  * Checkpoint Statement
  * ----------------------
  */
@@ -2498,7 +2509,6 @@ typedef struct ExecuteStmt
 {
 	NodeTag		type;
 	char	   *name;			/* The name of the plan to execute */
-	IntoClause *into;			/* Optional table to store results in */
 	List	   *params;			/* Values to assign to parameters */
 } ExecuteStmt;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 6685864..acaae3e 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -54,8 +54,6 @@ typedef struct PlannedStmt
 
 	Node	   *utilityStmt;	/* non-null if this is DECLARE CURSOR */
 
-	IntoClause *intoClause;		/* target for SELECT INTO / CREATE TABLE AS */
-
 	List	   *subplans;		/* Plan trees for SubPlan expressions */
 
 	Bitmapset  *rewindPlanIDs;	/* indices of subplans that require REWIND */
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 717ad79..eee27d3 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -3271,29 +3271,18 @@ exec_stmt_dynexecute(PLpgSQL_execstate *estate,
 			break;
 
 		case SPI_OK_SELINTO:
-
 			/*
 			 * We want to disallow SELECT INTO for now, because its behavior
 			 * is not consistent with SELECT INTO in a normal plpgsql context.
 			 * (We need to reimplement EXECUTE to parse the string as a
 			 * plpgsql command, not just feed it to SPI_execute.) However,
-			 * CREATE AS should be allowed ... and since it produces the same
-			 * parsetree as SELECT INTO, there's no way to tell the difference
-			 * except to look at the source text.  Wotta kluge!
+			 * CREATE AS should be allowed ...
 			 */
-			{
-				char	   *ptr;
-
-				for (ptr = querystr; *ptr; ptr++)
-					if (!scanner_isspace(*ptr))
-						break;
-				if (*ptr == 'S' || *ptr == 's')
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("EXECUTE of SELECT ... INTO is not implemented"),
-							 errhint("You might want to use EXECUTE ... INTO or EXECUTE CREATE TABLE ... AS instead.")));
-				break;
-			}
+			ereport(ERROR,
+			        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			         errmsg("EXECUTE of SELECT ... INTO is not implemented"),
+			         errhint("You might want to use EXECUTE ... INTO or EXECUTE CREATE TABLE ... AS instead.")));
+			break;
 
 			/* Some SPI errors deserve specific error messages */
 		case SPI_ERROR_COPY:
@@ -5733,7 +5722,7 @@ exec_simple_check_plan(PLpgSQL_expr *expr)
 	 */
 	if (!IsA(query, Query))
 		return;
-	if (query->commandType != CMD_SELECT || query->intoClause)
+	if (query->commandType != CMD_SELECT)
 		return;
 	if (query->rtable != NIL)
 		return;
@@ -5807,7 +5796,7 @@ exec_simple_recheck_plan(PLpgSQL_expr *expr, CachedPlan *cplan)
 	 */
 	if (!IsA(stmt, PlannedStmt))
 		return;
-	if (stmt->commandType != CMD_SELECT || stmt->intoClause)
+	if (stmt->commandType != CMD_SELECT)
 		return;
 	plan = stmt->planTree;
 	if (!IsA(plan, Result))
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index f49ec0e..2da50cf 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -139,7 +139,7 @@ SELECT * FROM writetest, temptest; -- ok
 (0 rows)
 
 CREATE TABLE test AS SELECT * FROM writetest; -- fail
-ERROR:  cannot execute SELECT INTO in a read-only transaction
+ERROR:  cannot execute CREATE TABLE AS in a read-only transaction
 START TRANSACTION READ WRITE;
 DROP TABLE writetest; -- ok
 COMMIT;
-- 
1.7.6.409.ge7a85.dirty

#22Ross Reedstrom
reedstrm@rice.edu
In reply to: Andres Freund (#15)
Re: Command Triggers

On Sat, Dec 03, 2011 at 01:26:22AM +0100, Andres Freund wrote:

On Saturday, December 03, 2011 01:09:48 AM Alvaro Herrera wrote:

Excerpts from Andres Freund's message of vie dic 02 19:09:47 -0300 2011:

Hi all,

There is also the point about how permission checks on the actual
commands (in comparison of modifying command triggers) and such are
handled:

BEFORE and INSTEAD will currently be called independently of the fact
whether the user is actually allowed to do said action (which is
inconsistent with data triggers) and indepentent of whether the object
they concern exists.

I wonder if anybody considers that a problem?

Hmm, we currently even have a patch (or is it already committed?) to
avoid locking objects before we know the user has permission on the
object. Getting to the point of calling the trigger would surely be
even worse.

Well, calling the trigger won't allow them to lock the object. It doesn't even
confirm the existance of the table.

didn't I see a discussion in passing about the possibility of using these command
triggers to implement some aspects of se-pgsql? In that case, you'd need the above
behavior.

Ross
--
Ross Reedstrom, Ph.D. reedstrm@rice.edu
Systems Engineer & Admin, Research Scientist phone: 713-348-6166
Connexions http://cnx.org fax: 713-348-3665
Rice University MS-375, Houston, TX 77005
GPG Key fingerprint = F023 82C8 9B0E 2CC6 0D8E F888 D3AE 810E 88F0 BEDE

#23Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Dimitri Fontaine (#12)
1 attachment(s)
Re: Command Triggers

Hi,

Please find an update attached, v4, fixing most remaining items. Next
steps are better docs and more commands support (along with finishing
currently supported ones), and a review locking behavior.

If you want to just scroll over the patch to get an impression of what's
in there rather than try out the attachment, follow this URL:

https://github.com/dimitri/postgres/compare/master...command_triggers

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

Will look into qualifying names.

I'm now qualifying relation names even if they have not been entered
with a namespace qualifier. What do you think? The other components
are left alone, I think the internal APIs for qualifying all kind of
objects from the parse tree and current context are mostly missing.

As an alternative it would be possible to pass the current search path but
that doesn't seem to the best approach to me;

The trigger runs with the same search_path settings as the command, right?

Maybe that's good enough for command triggers?

Command triggers should only be allowed for the database owner.

Yes, that needs to happen soon, along with pg_dump and psql support.

All three are implemented in the attached new revision of the patch.

Imo the current callsite in ProcessUtility isn't that good. I think it would
make more sense moving it into standard_ProcessUtility. It should be *after*
the check_xact_readonly check in there in my opinion.

Makes sense, will fix in the next revision.

Done too. It's better this way, thank you.

I am also don't think its a good idea to run the
ExecBeforeOrInsteadOfCommandTriggers stuff there because thats allso the path
that COMMIT/BEGIN and other stuff take. Those are pretty "fast path".

I suggest making two switches in standard_ProcessUtility, one for the non-
command trigger stuff which returns on success and one after that. Only after
the first triggers are checked.

Agreed.

That's about the way I've done it. Please note that doing it this way
means that a ProcessUtility_hook can decide whether or not the command
triggers are going to be fired or not, and that's true for BEFORE, AFTER
and INSTEAD OF command triggers. I think that's the way to go, though.

* Either tgenable's datatype or its readfunc is wrong (watch the compiler ;))
* ruleutils.c:
* generic routine for IF EXISTS, CASCADE, ...

Will have to wait until next revision.

Fixed. Well, the generic routine would only be called twice and would
only share a rather short expression, so will have to wait until I add
support for more commands.

There's a regression tests gotcha. Namely that the parallel running of
triggers against inheritance makes it impossible to predict if the
trigger on the command CREATE TABLE will spit out a notice in the
inherit tests. I don't know how that is usually avoided, but I guess it
involves picking some other command example that don't conflict with the
parallel tests of that section?

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

Attachments:

command-trigger.v4.patch.gzapplication/octet-streamDownload
#24Andres Freund
andres@anarazel.de
In reply to: Andres Freund (#21)
Re: Command Triggers

Hi Peter,

On Sunday, December 04, 2011 08:01:34 PM Andres Freund wrote:

On Sunday, December 04, 2011 05:34:44 PM Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

I have two questions now:

First, does anybody think it would be worth getting rid of the
duplication from OpenIntoRel (formerly from execMain.c) in regard to
DefineRelation()?

That's probably reasonable to do, since as you say it would remove the
opportunity for bugs-of-omission in the CTAS table creation step.
OTOH, if you find yourself having to make any significant changes to
DefineRelation, then maybe not.

Building a CreateStmt seems to work well enough so far.
The only problem with that approach so far that I found is that:

CREATE TABLE collate_test2
(
a int,
b text COLLATE "POSIX"
);

CREATE TABLE collate_test1
(
a int,
b text COLLATE "C" NOT NULL
);

CREATE TABLE test_u AS SELECT a, b FROM collate_test1 UNION ALL SELECT a,
b FROM collate_test2; -- fail

failed with:
ERROR: no collation was derived for column "b" with collatable type text
HINT: Use the COLLATE clause to set the collation explicitly.
"works" now.

Could you explain why the above should fail? After all the UNION is valid
outside the CREATE TABLE and you can even sort on b.

That tidbit is he only thing I couldn't quickly solve since the last
submission...

Andres

#25Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Andres Freund (#10)
Re: Command Triggers

Hi,

Andres Freund <andres@anarazel.de> writes:

Hm. I just noticed a relatively big hole in the patch: The handling of
deletion of dependent objects currently is nonexistant because they don't go
through ProcessUtility...

That's the reason why we're talking about “command triggers” rather than
“DDL triggers”. We don't intend to fire the triggers at each DDL
operation happening on the server, but for each command.

This restriction still allows us to provide a very useful feature when
checked against the main use cases we target here. Those are auditing,
and replication (the replay will also CASCADEs), and a generic enough
SUDO facility (because the trigger function can well be SECURITY
DEFINER).

We could also add a 'cascading bool' parameter to the trigger function
API and have that always false in 9.2, then choose what to fill the
other parameters with in a later release. The obvious risk would be to
decide that we need another API, then we didn't make a good move after
all.

My current feeling and vote is thus to leave that alone and document the
restriction.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#26Peter Eisentraut
peter_e@gmx.net
In reply to: Andres Freund (#24)
Re: Command Triggers

On sön, 2011-12-11 at 04:26 +0100, Andres Freund wrote:

Building a CreateStmt seems to work well enough so far.
The only problem with that approach so far that I found is that:

CREATE TABLE collate_test2
(
a int,
b text COLLATE "POSIX"
);

CREATE TABLE collate_test1
(
a int,
b text COLLATE "C" NOT NULL
);

CREATE TABLE test_u AS SELECT a, b FROM collate_test1 UNION ALL SELECT a,
b FROM collate_test2; -- fail

failed with:
ERROR: no collation was derived for column "b" with collatable type text
HINT: Use the COLLATE clause to set the collation explicitly.
"works" now.

Could you explain why the above should fail? After all the UNION is valid
outside the CREATE TABLE and you can even sort on b.

That would be strange, because earlier in the test file there is also

SELECT a, b FROM collate_test1 UNION ALL SELECT a, b FROM collate_test2 ORDER BY 2; -- fail

The union itself is valid, but because it combines two different
collations, the collation derivation for the column is "unknown", and so
it cannot be ordered. And we made the implementation decision to not
allow creating columns with unknown collation.

#27Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#26)
1 attachment(s)
Re: Command Triggers

On Sunday, December 11, 2011 08:09:36 PM Peter Eisentraut wrote:

On sön, 2011-12-11 at 04:26 +0100, Andres Freund wrote:

Building a CreateStmt seems to work well enough so far.
The only problem with that approach so far that I found is that:

CREATE TABLE collate_test2
(

a int,

b text COLLATE "POSIX"

);

CREATE TABLE collate_test1
(

a int,

b text COLLATE "C" NOT NULL

);

CREATE TABLE test_u AS SELECT a, b FROM collate_test1 UNION ALL SELECT
a, b FROM collate_test2; -- fail

failed with:
ERROR: no collation was derived for column "b" with collatable type
text HINT: Use the COLLATE clause to set the collation explicitly.
"works" now.

Could you explain why the above should fail? After all the UNION is valid
outside the CREATE TABLE and you can even sort on b.

That would be strange, because earlier in the test file there is also

SELECT a, b FROM collate_test1 UNION ALL SELECT a, b FROM collate_test2
ORDER BY 2; -- fail

Yea. I didn't realize that it only fails during execution if there actually
are rows present (confusing behaviour btw).

The union itself is valid, but because it combines two different
collations, the collation derivation for the column is "unknown", and so
it cannot be ordered. And we made the implementation decision to not
allow creating columns with unknown collation.

I copied that behaviour. Its a bit complicated by the fact that
GetColumnDefCollation cannot be taught to accept an invalidOid so I had to
duplicate the check in CreateTableAs...

I attached the - from my side - final version of the patch. I dislike two
things about it:
* code duplication due to error handling. Before making the error message for
various illegal SELECT INTOs the patch actually shrank the code size... If
anybody has a good idea to avoid duplicating that loop around SelectStmt->ops
I would be happy.
* new executor flags to define whether oids should be returned

Andres

Attachments:

0001-Transform-CREATE-TABLE-AS-SELECT-INTO-into-a-utility.patchtext/x-patch; charset=UTF-8; name=0001-Transform-CREATE-TABLE-AS-SELECT-INTO-into-a-utility.patchDownload
From d8fb1f9adbddd1eef123d66a89a9fc0ecd96f60b Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Sun, 4 Dec 2011 19:51:31 +0100
Subject: [PATCH] Transform CREATE TABLE AS/SELECT INTO into a utility
 statement

---
 src/backend/commands/copy.c                |   20 +-
 src/backend/commands/prepare.c             |   69 +----
 src/backend/commands/tablecmds.c           |  332 ++++++++++++++++++++++
 src/backend/commands/view.c                |   20 +-
 src/backend/executor/execMain.c            |  414 ++--------------------------
 src/backend/executor/execUtils.c           |    2 -
 src/backend/executor/functions.c           |    4 +-
 src/backend/executor/spi.c                 |   10 +-
 src/backend/nodes/copyfuncs.c              |   18 +-
 src/backend/nodes/equalfuncs.c             |   16 +-
 src/backend/nodes/outfuncs.c               |    4 +-
 src/backend/nodes/readfuncs.c              |    1 -
 src/backend/optimizer/plan/planner.c       |    1 -
 src/backend/optimizer/plan/subselect.c     |    1 -
 src/backend/optimizer/prep/prepjointree.c  |    6 +-
 src/backend/optimizer/util/clauses.c       |    4 +-
 src/backend/parser/analyze.c               |  113 +++++---
 src/backend/parser/gram.y                  |   28 ++-
 src/backend/parser/parse_clause.c          |   21 +-
 src/backend/parser/parse_cte.c             |   21 +-
 src/backend/parser/parse_expr.c            |   21 +-
 src/backend/parser/parser.c                |   38 +++
 src/backend/rewrite/rewriteDefine.c        |    3 +-
 src/backend/tcop/pquery.c                  |   13 +-
 src/backend/tcop/utility.c                 |   46 ++--
 src/include/access/htup.h                  |    2 +-
 src/include/commands/prepare.h             |    3 +
 src/include/commands/tablecmds.h           |    4 +
 src/include/executor/executor.h            |    5 +
 src/include/nodes/execnodes.h              |    9 +-
 src/include/nodes/nodes.h                  |    1 +
 src/include/nodes/parsenodes.h             |   20 +-
 src/include/nodes/plannodes.h              |    2 -
 src/pl/plpgsql/src/pl_exec.c               |   27 +--
 src/test/regress/expected/select_into.out  |   19 ++
 src/test/regress/expected/transactions.out |    2 +-
 src/test/regress/sql/select_into.sql       |   15 +
 37 files changed, 717 insertions(+), 618 deletions(-)

diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 9c994ef..6ace292 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -1190,6 +1190,20 @@ BeginCopy(bool is_from,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("COPY (SELECT) WITH OIDS is not supported")));
 
+
+		/* Query mustn't use INTO, either */
+		if(IsA(raw_query, SelectStmt))
+		{
+			SelectStmt* first = (SelectStmt*)raw_query;
+			while (first && first->op != SETOP_NONE)
+				first = first->larg;
+
+			if (first->intoClause)
+				ereport(ERROR,
+				        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				         errmsg("COPY (SELECT INTO) is not supported")));
+		}
+
 		/*
 		 * Run parse analysis and rewrite.	Note this also acquires sufficient
 		 * locks on the source table(s).
@@ -1211,12 +1225,6 @@ BeginCopy(bool is_from,
 		Assert(query->commandType == CMD_SELECT);
 		Assert(query->utilityStmt == NULL);
 
-		/* Query mustn't use INTO, either */
-		if (query->intoClause)
-			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("COPY (SELECT INTO) is not supported")));
-
 		/* plan the query */
 		plan = planner(query, 0, NULL);
 
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index a949215..61a5f89 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -43,8 +43,6 @@
 static HTAB *prepared_queries = NULL;
 
 static void InitQueryHashTable(void);
-static ParamListInfo EvaluateParams(PreparedStatement *pstmt, List *params,
-			   const char *queryString, EState *estate);
 static Datum build_regtype_array(Oid *param_types, int num_params);
 
 /*
@@ -177,6 +175,9 @@ PrepareQuery(PrepareStmt *stmt, const char *queryString)
  * EXECUTE, which we might need for error reporting while processing the
  * parameter expressions.  The query_string that we copy from the plan
  * source is that of the original PREPARE.
+ *
+ * If you add additional things here you should check whether
+ * CreateTableAs also needs them.
  */
 void
 ExecuteQuery(ExecuteStmt *stmt, const char *queryString,
@@ -222,51 +223,9 @@ ExecuteQuery(ExecuteStmt *stmt, const char *queryString,
 	query_string = MemoryContextStrdup(PortalGetHeapMemory(portal),
 									   entry->plansource->query_string);
 
-	/*
-	 * For CREATE TABLE / AS EXECUTE, we must make a copy of the stored query
-	 * so that we can modify its destination (yech, but this has always been
-	 * ugly).  For regular EXECUTE we can just use the cached query, since the
-	 * executor is read-only.
-	 */
-	if (stmt->into)
-	{
-		MemoryContext oldContext;
-		PlannedStmt *pstmt;
-
-		/* Replan if needed, and increment plan refcount transiently */
-		cplan = GetCachedPlan(entry->plansource, paramLI, true);
-
-		/* Copy plan into portal's context, and modify */
-		oldContext = MemoryContextSwitchTo(PortalGetHeapMemory(portal));
-
-		plan_list = copyObject(cplan->stmt_list);
-
-		if (list_length(plan_list) != 1)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("prepared statement is not a SELECT")));
-		pstmt = (PlannedStmt *) linitial(plan_list);
-		if (!IsA(pstmt, PlannedStmt) ||
-			pstmt->commandType != CMD_SELECT ||
-			pstmt->utilityStmt != NULL)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg("prepared statement is not a SELECT")));
-		pstmt->intoClause = copyObject(stmt->into);
-
-		MemoryContextSwitchTo(oldContext);
-
-		/* We no longer need the cached plan refcount ... */
-		ReleaseCachedPlan(cplan, true);
-		/* ... and we don't want the portal to depend on it, either */
-		cplan = NULL;
-	}
-	else
-	{
-		/* Replan if needed, and increment plan refcount for portal */
-		cplan = GetCachedPlan(entry->plansource, paramLI, false);
-		plan_list = cplan->stmt_list;
-	}
+	/* Replan if needed, and increment plan refcount for portal */
+	cplan = GetCachedPlan(entry->plansource, paramLI, false);
+	plan_list = cplan->stmt_list;
 
 	PortalDefineQuery(portal,
 					  NULL,
@@ -302,7 +261,7 @@ ExecuteQuery(ExecuteStmt *stmt, const char *queryString,
  * CreateQueryDesc(), which allows the executor to make use of the parameters
  * during query execution.
  */
-static ParamListInfo
+ParamListInfo
 EvaluateParams(PreparedStatement *pstmt, List *params,
 			   const char *queryString, EState *estate)
 {
@@ -666,20 +625,6 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, ExplainState *es,
 
 		if (IsA(pstmt, PlannedStmt))
 		{
-			if (execstmt->into)
-			{
-				if (pstmt->commandType != CMD_SELECT ||
-					pstmt->utilityStmt != NULL)
-					ereport(ERROR,
-							(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-							 errmsg("prepared statement is not a SELECT")));
-
-				/* Copy the stmt so we can modify it */
-				pstmt = copyObject(pstmt);
-
-				pstmt->intoClause = execstmt->into;
-			}
-
 			ExplainOnePlan(pstmt, es, query_string, paramLI);
 		}
 		else
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c4622c0..d530cd5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -44,6 +44,7 @@
 #include "commands/cluster.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
+#include "commands/prepare.h"
 #include "commands/sequence.h"
 #include "commands/tablecmds.h"
 #include "commands/tablespace.h"
@@ -68,6 +69,7 @@
 #include "parser/parser.h"
 #include "rewrite/rewriteDefine.h"
 #include "rewrite/rewriteHandler.h"
+#include "tcop/tcopprot.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
 #include "storage/lock.h"
@@ -195,6 +197,18 @@ struct dropmsgstrings
 	const char *drophint_msg;
 };
 
+/*
+ * Support structure for CTAS
+ */
+typedef struct
+{
+	DestReceiver pub;			/* publicly-known function pointers */
+	EState	   *estate;			/* EState we are working with */
+	Relation	rel;			/* Relation to write to */
+	int			hi_options;		/* heap_insert performance options */
+	BulkInsertState bistate;	/* bulk insert state */
+} DR_intorel;
+
 static const struct dropmsgstrings dropmsgstringarray[] = {
 	{RELKIND_RELATION,
 		ERRCODE_UNDEFINED_TABLE,
@@ -640,6 +654,235 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId)
 	return relationId;
 }
 
+void
+CreateTableAs(CreateTableAsStmt *stmt, const char *queryString,
+              ParamListInfo params, DestReceiver *dest){
+	IntoClause *into = stmt->into;
+	Query *query;
+	List *rewritten;
+	PlannedStmt *plan;
+	QueryDesc *queryDesc;
+	Relation intoRelationDesc;
+	CreateStmt *create;
+	int natt;
+	Oid intoRelationId;
+	ListCell *lc;
+	int eflags = 0;
+	EState *param_estate = NULL;
+	CachedPlan *cplan = NULL;
+
+	Assert(IsA(stmt->query, Query));
+	query = (Query*)stmt->query;
+
+	/*
+	 * Construct plans for the supported
+	 */
+	if(query->commandType == CMD_SELECT){
+		rewritten = QueryRewrite((Query *) copyObject(stmt->query));
+
+		if(!rewritten)
+			elog(ERROR, "CREATE TABLE AS/SELECT INTO query rewrote to nothing");
+
+		if(list_length(rewritten) != 1)
+			elog(ERROR, "CREATE TABLE AS/SELECT INTO query rewrote to more than one query");
+
+		plan = pg_plan_query(lfirst(list_head(rewritten)), 0, params);
+	}
+	else if(query->commandType == CMD_UTILITY &&
+	        IsA(query->utilityStmt, ExecuteStmt)){
+		/*
+		 * We cannot easily share more infrastructure with prepare.c's
+		 * ExecuteQuery because teaching it to return a started
+		 * executor would amount to about the same amount of
+		 * duplication as doing it here.
+		 *
+		 */
+		ExecuteStmt *stmt;
+		PreparedStatement *entry;
+		List *plan_list;
+		ParamListInfo paramLI = NULL;
+
+		Assert(IsA(query->utilityStmt, ExecuteStmt));
+		stmt = (ExecuteStmt*)(query->utilityStmt);
+
+		entry = FetchPreparedStatement(stmt->name, true);
+
+		/* Shouldn't find a non-fixed-result cached plan */
+		if (!entry->plansource->fixed_result)
+			elog(ERROR, "EXECUTE does not support variable-result cached plans");
+
+		/* Evaluate parameters, if any */
+		if (entry->plansource->num_params > 0)
+		{
+			param_estate = CreateExecutorState();
+			param_estate->es_param_list_info = params;
+			paramLI = EvaluateParams(entry, stmt->params,
+			                         queryString, param_estate);
+
+		}
+
+		cplan = GetCachedPlan(entry->plansource, paramLI, true);
+		plan_list = cplan->stmt_list;
+
+		if (list_length(plan_list) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("prepared statement is not a SELECT")));
+
+		plan = (PlannedStmt *) linitial(plan_list);
+		if (!IsA(plan, PlannedStmt) ||
+			plan->commandType != CMD_SELECT ||
+			plan->utilityStmt != NULL)
+			ereport(ERROR,
+			        (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+			         errmsg("prepared statement is not a SELECT")));
+
+		queryString = pstrdup(entry->plansource->query_string);
+	}
+	else{
+		elog(ERROR, "unsupported command for CREATE TABLE AS");
+		abort();
+	}
+
+
+	/*
+	 * Use a snapshot with an updated command ID to ensure this query sees
+	 * results of any previously executed queries.
+	 */
+	PushCopiedSnapshot(GetActiveSnapshot());
+	UpdateActiveSnapshotCommandId();
+
+	queryDesc = CreateQueryDesc(plan, queryString,
+								GetActiveSnapshot(), InvalidSnapshot,
+								CreateDestReceiver(DestIntoRel), params,
+	                            false);
+
+	/*
+	 * We need to tell the executor whether it has to produce oids or
+	 * not because it doesn't have enough information to do so itself
+	 * as were not telling it that its inserting into a table (because
+	 * that would be too complicated for now)
+	 */
+	if(interpretOidsOption(into->options))
+		eflags |= EXEC_FLAG_FORCE_OIDS_ON;
+	else
+		eflags |= EXEC_FLAG_FORCE_OIDS_OFF;
+
+	/*
+	 * call ExecutorStart to prepare the plan for execution. Only
+	 * after this we have enough information to actually create a
+	 * target relation
+	 */
+	ExecutorStart(queryDesc, eflags);
+
+	/*
+	 * create the target relation
+	 */
+	create = makeNode(CreateStmt);
+	create->relation = into->rel;
+	create->options = into->options;
+	create->oncommit = into->onCommit;
+	create->tablespacename = into->tableSpaceName;
+
+	/*
+	 * If a column name list was specified in CREATE TABLE AS,
+	 * override the column names derived from the query.  (Too few
+	 * column names are OK, too many are not.)
+	 */
+	if (list_length(into->colNames) > queryDesc->tupDesc->natts)
+		ereport(ERROR,
+		        (errcode(ERRCODE_SYNTAX_ERROR),
+		         errmsg("CREATE TABLE AS specifies too many column names")));
+	lc = list_head(into->colNames);
+	for(natt = 0; natt < queryDesc->tupDesc->natts; natt++){
+		ColumnDef *col = makeNode(ColumnDef);
+		TypeName *type = makeNode(TypeName);
+
+		Form_pg_attribute attribute = queryDesc->tupDesc->attrs[natt];
+
+		if(!lc)
+			col->colname = NameStr(attribute->attname);
+		else{
+			col->colname = strVal(lfirst(lc));
+			lc = lnext(lc);
+		}
+
+		col->collOid = attribute->attcollation;
+
+		type->typeOid = attribute->atttypid;
+		type->typemod = attribute->atttypmod;
+		col->typeName= type;
+
+		/*
+		 * We check the column type here because collOid=InvalidOid
+		 * denotes an invalid oid for a collatable type in
+		 * queryDesc->tupDesc but GetColumnDefCollation - which will
+		 * be called by DefineRelation - defers the default collation
+		 * out of that...
+		 * XXX: A better solution would be welcome.
+		 */
+		CheckAttributeType(col->colname, col->typeName->typeOid, col->collOid,
+		                   NULL, false);
+		create->tableElts = lappend(create->tableElts, col);
+	}
+
+	/*
+	 * Actually create the target table
+	 */
+	intoRelationId = DefineRelation(create, RELKIND_RELATION, InvalidOid);
+	intoRelationDesc = heap_open(intoRelationId, AccessExclusiveLock);
+
+
+	if(!into->skipData){
+		DR_intorel *myState = (DR_intorel *) queryDesc->dest;
+		/* setup the target of the DestIntoRel receiver */
+		myState->estate = queryDesc->estate;
+		myState->rel = intoRelationDesc;
+
+		/* run the plan */
+		ExecutorRun(queryDesc, ForwardScanDirection, 0L);
+
+	}
+
+	/*
+	 * Check INSERT permission on the constructed table.
+	 *
+	 * XXX: It would arguable make sense to do this check only if
+	 * into->skipData is true.
+	 */
+	{
+		RangeTblEntry *rte = makeNode(RangeTblEntry);
+		rte->rtekind = RTE_RELATION;
+		rte->relid = intoRelationId;
+		rte->relkind = RELKIND_RELATION;
+		rte->requiredPerms = ACL_INSERT;
+
+		for (natt = 1; natt <= intoRelationDesc->rd_att->natts; natt++)
+			rte->modifiedCols = bms_add_member(rte->modifiedCols,
+			                                   natt - FirstLowInvalidHeapAttributeNumber);
+
+		ExecCheckRTPerms(list_make1(rte), true);
+	}
+
+	/* run cleanup too */
+	ExecutorFinish(queryDesc);
+	ExecutorEnd(queryDesc);
+
+	/* close rel, but keep lock until commit */
+	heap_close(intoRelationDesc, NoLock);
+
+	FreeQueryDesc(queryDesc);
+
+	if(cplan){
+		ReleaseCachedPlan(cplan, true);
+
+		if (param_estate)
+			FreeExecutorState(param_estate);
+	}
+
+	PopActiveSnapshot();
+}
+
 /*
  * Emit the right error or warning message for a "DROP" command issued on a
  * non-existent relation
@@ -9771,3 +10014,92 @@ AtEOSubXact_on_commit_actions(bool isCommit, SubTransactionId mySubid,
 		}
 	}
 }
+
+/*
+ * Support for SELECT INTO (a/k/a CREATE TABLE AS)
+ *
+ * We implement SELECT INTO by diverting SELECT's normal output with
+ * a specialized DestReceiver type.
+ */
+
+/*
+ * intorel_startup --- executor startup
+ */
+static void
+intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	DR_intorel *myState = (DR_intorel *) self;
+	myState->bistate = GetBulkInsertState();
+}
+
+/*
+ * intorel_receive --- receive one tuple
+ */
+static void
+intorel_receive(TupleTableSlot *slot, DestReceiver *self)
+{
+	DR_intorel *myState = (DR_intorel *) self;
+	HeapTuple	tuple;
+
+	/*
+	 * get the heap tuple out of the tuple table slot, making sure we have a
+	 * writable copy
+	 */
+	tuple = ExecMaterializeSlot(slot);
+
+	/*
+	 * force assignment of new OID (see comments in ExecInsert)
+	 */
+	if (myState->rel->rd_rel->relhasoids)
+		HeapTupleSetOid(tuple, InvalidOid);
+
+	heap_insert(myState->rel,
+				tuple,
+				myState->estate->es_output_cid,
+				myState->hi_options,
+				myState->bistate);
+
+	/* We know this is a newly created relation, so there are no indexes */
+}
+
+/*
+ * intorel_shutdown --- executor end
+ */
+static void
+intorel_shutdown(DestReceiver *self)
+{
+	DR_intorel *myState = (DR_intorel *) self;
+
+	FreeBulkInsertState(myState->bistate);
+	if (myState->hi_options & HEAP_INSERT_SKIP_WAL)
+		heap_sync(myState->rel);
+}
+
+/*
+ * intorel_destroy --- release DestReceiver object
+ */
+static void
+intorel_destroy(DestReceiver *self)
+{
+	pfree(self);
+}
+
+/*
+ * CreateIntoRelDestReceiver -- create a suitable DestReceiver object
+ *
+ */
+DestReceiver *
+CreateIntoRelDestReceiver(void)
+{
+	DR_intorel *self = (DR_intorel *) palloc0(sizeof(DR_intorel));
+
+	self->pub.receiveSlot = intorel_receive;
+	self->pub.rStartup = intorel_startup;
+	self->pub.rShutdown = intorel_shutdown;
+	self->pub.rDestroy = intorel_destroy;
+	self->pub.mydest = DestIntoRel;
+
+	/* private fields will be set by OpenIntoRel */
+
+	return (DestReceiver *) self;
+}
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index b238199..5977092 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -420,6 +420,22 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	RangeVar   *view;
 
 	/*
+	 * We check for SELECT INTO before parse analysis because that
+	 * would complain about the SELECT INTO without specific enough detail.
+	 */
+	if(IsA(stmt->query, SelectStmt))
+	{
+		SelectStmt* first = (SelectStmt*)stmt->query;
+		while (first && first->op != SETOP_NONE)
+			first = first->larg;
+
+		if (first->intoClause)
+			ereport(ERROR,
+			        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			         errmsg("views must not contain SELECT INTO")));
+	}
+
+	/*
 	 * Run parse analysis to convert the raw parse tree to a Query.  Note this
 	 * also acquires sufficient locks on the source table(s).
 	 *
@@ -441,10 +457,6 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	 * DefineQueryRewrite(), but that function will complain about a bogus ON
 	 * SELECT rule, and we'd rather the message complain about a view.
 	 */
-	if (viewParse->intoClause != NULL)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("views must not contain SELECT INTO")));
 	if (viewParse->hasModifyingCTE)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index d19e097..6b2cf4c 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -87,12 +87,6 @@ static bool ExecCheckRTEPerms(RangeTblEntry *rte);
 static void ExecCheckXactReadOnly(PlannedStmt *plannedstmt);
 static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate,
 				  Plan *planTree);
-static void OpenIntoRel(QueryDesc *queryDesc);
-static void CloseIntoRel(QueryDesc *queryDesc);
-static void intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo);
-static void intorel_receive(TupleTableSlot *slot, DestReceiver *self);
-static void intorel_shutdown(DestReceiver *self);
-static void intorel_destroy(DestReceiver *self);
 
 /* end of local decls */
 
@@ -171,11 +165,10 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 		case CMD_SELECT:
 
 			/*
-			 * SELECT INTO, SELECT FOR UPDATE/SHARE and modifying CTEs need to
+			 * SELECT FOR UPDATE/SHARE and modifying CTEs need to
 			 * mark tuples
 			 */
-			if (queryDesc->plannedstmt->intoClause != NULL ||
-				queryDesc->plannedstmt->rowMarks != NIL ||
+			if (queryDesc->plannedstmt->rowMarks != NIL ||
 				queryDesc->plannedstmt->hasModifyingCTE)
 				estate->es_output_cid = GetCurrentCommandId(true);
 
@@ -209,6 +202,13 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	estate->es_top_eflags = eflags;
 	estate->es_instrument = queryDesc->instrument_options;
 
+	if(eflags & EXEC_FLAG_FORCE_OIDS_ON)
+		estate->es_force_oids = ESTATE_FORCE_OIDS_ON;
+	else if(eflags & EXEC_FLAG_FORCE_OIDS_OFF)
+		estate->es_force_oids = ESTATE_FORCE_OIDS_OFF;
+	else
+		estate->es_force_oids = ESTATE_FORCE_OIDS_DEFAULT;
+
 	/*
 	 * Initialize the plan state tree
 	 */
@@ -307,13 +307,6 @@ standard_ExecutorRun(QueryDesc *queryDesc,
 		(*dest->rStartup) (dest, operation, queryDesc->tupDesc);
 
 	/*
-	 * if it's CREATE TABLE AS ... WITH NO DATA, skip plan execution
-	 */
-	if (estate->es_select_into &&
-		queryDesc->plannedstmt->intoClause->skipData)
-		direction = NoMovementScanDirection;
-
-	/*
 	 * run plan
 	 */
 	if (!ScanDirectionIsNoMovement(direction))
@@ -448,12 +441,6 @@ standard_ExecutorEnd(QueryDesc *queryDesc)
 
 	ExecEndPlan(queryDesc->planstate, estate);
 
-	/*
-	 * Close the SELECT INTO relation if any
-	 */
-	if (estate->es_select_into)
-		CloseIntoRel(queryDesc);
-
 	/* do away with our snapshots */
 	UnregisterSnapshot(estate->es_snapshot);
 	UnregisterSnapshot(estate->es_crosscheck_snapshot);
@@ -703,15 +690,6 @@ ExecCheckXactReadOnly(PlannedStmt *plannedstmt)
 {
 	ListCell   *l;
 
-	/*
-	 * CREATE TABLE AS or SELECT INTO?
-	 *
-	 * XXX should we allow this if the destination is temp?  Considering that
-	 * it would still require catalog changes, probably not.
-	 */
-	if (plannedstmt->intoClause != NULL)
-		PreventCommandIfReadOnly(CreateCommandTag((Node *) plannedstmt));
-
 	/* Fail if write permissions are requested on any non-temp table */
 	foreach(l, plannedstmt->rtable)
 	{
@@ -861,18 +839,6 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 	}
 
 	/*
-	 * Detect whether we're doing SELECT INTO.  If so, set the es_into_oids
-	 * flag appropriately so that the plan tree will be initialized with the
-	 * correct tuple descriptors.  (Other SELECT INTO stuff comes later.)
-	 */
-	estate->es_select_into = false;
-	if (operation == CMD_SELECT && plannedstmt->intoClause != NULL)
-	{
-		estate->es_select_into = true;
-		estate->es_into_oids = interpretOidsOption(plannedstmt->intoClause->options);
-	}
-
-	/*
 	 * Initialize the executor's tuple table to empty.
 	 */
 	estate->es_tupleTable = NIL;
@@ -923,9 +889,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 	planstate = ExecInitNode(plan, estate, eflags);
 
 	/*
-	 * Get the tuple descriptor describing the type of tuples to return. (this
-	 * is especially important if we are creating a relation with "SELECT
-	 * INTO")
+	 * Get the tuple descriptor describing the type of tuples to return.
 	 */
 	tupType = ExecGetResultType(planstate);
 
@@ -965,16 +929,6 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 
 	queryDesc->tupDesc = tupType;
 	queryDesc->planstate = planstate;
-
-	/*
-	 * If doing SELECT INTO, initialize the "into" relation.  We must wait
-	 * till now so we have the "clean" result tuple type to create the new
-	 * table from.
-	 *
-	 * If EXPLAIN, skip creating the "into" relation.
-	 */
-	if (estate->es_select_into && !(eflags & EXEC_FLAG_EXPLAIN_ONLY))
-		OpenIntoRel(queryDesc);
 }
 
 /*
@@ -1227,7 +1181,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 /*
  *		ExecContextForcesOids
  *
- * This is pretty grotty: when doing INSERT, UPDATE, or SELECT INTO,
+ * This is pretty grotty: when doing INSERT or UPDATE
  * we need to ensure that result tuples have space for an OID iff they are
  * going to be stored into a relation that has OIDs.  In other contexts
  * we are free to choose whether to leave space for OIDs in result tuples
@@ -1252,15 +1206,25 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
  * the ModifyTable node, so ModifyTable has to set es_result_relation_info
  * while initializing each subplan.
  *
- * SELECT INTO is even uglier, because we don't have the INTO relation's
- * descriptor available when this code runs; we have to look aside at a
- * flag set by InitPlan().
+ * SELECT INTO is even uglier, because we don't have the INTO
+ * relation's descriptor available - because its only defined after
+ * the query started - when this code runs; we have to look aside at
+ * flags which is passed to ExecutorStart via eflags.
  */
 bool
 ExecContextForcesOids(PlanState *planstate, bool *hasoids)
 {
 	ResultRelInfo *ri = planstate->state->es_result_relation_info;
 
+	if(planstate->state->es_force_oids == ESTATE_FORCE_OIDS_ON){
+		*hasoids = true;
+		return true;
+	}
+	else if(planstate->state->es_force_oids == ESTATE_FORCE_OIDS_OFF){
+		*hasoids = false;
+		return true;
+	}
+
 	if (ri != NULL)
 	{
 		Relation	rel = ri->ri_RelationDesc;
@@ -1272,12 +1236,6 @@ ExecContextForcesOids(PlanState *planstate, bool *hasoids)
 		}
 	}
 
-	if (planstate->state->es_select_into)
-	{
-		*hasoids = planstate->state->es_into_oids;
-		return true;
-	}
-
 	return false;
 }
 
@@ -2224,8 +2182,7 @@ EvalPlanQualStart(EPQState *epqstate, EState *parentestate, Plan *planTree)
 	estate->es_rowMarks = parentestate->es_rowMarks;
 	estate->es_top_eflags = parentestate->es_top_eflags;
 	estate->es_instrument = parentestate->es_instrument;
-	estate->es_select_into = parentestate->es_select_into;
-	estate->es_into_oids = parentestate->es_into_oids;
+
 	/* es_auxmodifytables must NOT be copied */
 
 	/*
@@ -2366,324 +2323,3 @@ EvalPlanQualEnd(EPQState *epqstate)
 	epqstate->planstate = NULL;
 	epqstate->origslot = NULL;
 }
-
-
-/*
- * Support for SELECT INTO (a/k/a CREATE TABLE AS)
- *
- * We implement SELECT INTO by diverting SELECT's normal output with
- * a specialized DestReceiver type.
- */
-
-typedef struct
-{
-	DestReceiver pub;			/* publicly-known function pointers */
-	EState	   *estate;			/* EState we are working with */
-	Relation	rel;			/* Relation to write to */
-	int			hi_options;		/* heap_insert performance options */
-	BulkInsertState bistate;	/* bulk insert state */
-} DR_intorel;
-
-/*
- * OpenIntoRel --- actually create the SELECT INTO target relation
- *
- * This also replaces QueryDesc->dest with the special DestReceiver for
- * SELECT INTO.  We assume that the correct result tuple type has already
- * been placed in queryDesc->tupDesc.
- */
-static void
-OpenIntoRel(QueryDesc *queryDesc)
-{
-	IntoClause *into = queryDesc->plannedstmt->intoClause;
-	EState	   *estate = queryDesc->estate;
-	TupleDesc	intoTupDesc = queryDesc->tupDesc;
-	Relation	intoRelationDesc;
-	char	   *intoName;
-	Oid			namespaceId;
-	Oid			tablespaceId;
-	Datum		reloptions;
-	Oid			intoRelationId;
-	DR_intorel *myState;
-	RangeTblEntry  *rte;
-	AttrNumber		attnum;
-	static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
-
-	Assert(into);
-
-	/*
-	 * XXX This code needs to be kept in sync with DefineRelation(). Maybe we
-	 * should try to use that function instead.
-	 */
-
-	/*
-	 * Check consistency of arguments
-	 */
-	if (into->onCommit != ONCOMMIT_NOOP
-		&& into->rel->relpersistence != RELPERSISTENCE_TEMP)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("ON COMMIT can only be used on temporary tables")));
-
-	/*
-	 * If a column name list was specified in CREATE TABLE AS, override the
-	 * column names derived from the query.  (Too few column names are OK, too
-	 * many are not.)  It would probably be all right to scribble directly on
-	 * the query's result tupdesc, but let's be safe and make a copy.
-	 */
-	if (into->colNames)
-	{
-		ListCell   *lc;
-
-		intoTupDesc = CreateTupleDescCopy(intoTupDesc);
-		attnum = 1;
-		foreach(lc, into->colNames)
-		{
-			char	   *colname = strVal(lfirst(lc));
-
-			if (attnum > intoTupDesc->natts)
-				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("CREATE TABLE AS specifies too many column names")));
-			namestrcpy(&(intoTupDesc->attrs[attnum - 1]->attname), colname);
-			attnum++;
-		}
-	}
-
-	/*
-	 * Find namespace to create in, check its permissions
-	 */
-	intoName = into->rel->relname;
-	namespaceId = RangeVarGetAndCheckCreationNamespace(into->rel);
-	RangeVarAdjustRelationPersistence(into->rel, namespaceId);
-
-	/*
-	 * Security check: disallow creating temp tables from security-restricted
-	 * code.  This is needed because calling code might not expect untrusted
-	 * tables to appear in pg_temp at the front of its search path.
-	 */
-	if (into->rel->relpersistence == RELPERSISTENCE_TEMP
-		&& InSecurityRestrictedOperation())
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("cannot create temporary table within security-restricted operation")));
-
-	/*
-	 * Select tablespace to use.  If not specified, use default tablespace
-	 * (which may in turn default to database's default).
-	 */
-	if (into->tableSpaceName)
-	{
-		tablespaceId = get_tablespace_oid(into->tableSpaceName, false);
-	}
-	else
-	{
-		tablespaceId = GetDefaultTablespace(into->rel->relpersistence);
-		/* note InvalidOid is OK in this case */
-	}
-
-	/* Check permissions except when using the database's default space */
-	if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace)
-	{
-		AclResult	aclresult;
-
-		aclresult = pg_tablespace_aclcheck(tablespaceId, GetUserId(),
-										   ACL_CREATE);
-
-		if (aclresult != ACLCHECK_OK)
-			aclcheck_error(aclresult, ACL_KIND_TABLESPACE,
-						   get_tablespace_name(tablespaceId));
-	}
-
-	/* Parse and validate any reloptions */
-	reloptions = transformRelOptions((Datum) 0,
-									 into->options,
-									 NULL,
-									 validnsps,
-									 true,
-									 false);
-	(void) heap_reloptions(RELKIND_RELATION, reloptions, true);
-
-	/* Now we can actually create the new relation */
-	intoRelationId = heap_create_with_catalog(intoName,
-											  namespaceId,
-											  tablespaceId,
-											  InvalidOid,
-											  InvalidOid,
-											  InvalidOid,
-											  GetUserId(),
-											  intoTupDesc,
-											  NIL,
-											  RELKIND_RELATION,
-											  into->rel->relpersistence,
-											  false,
-											  false,
-											  true,
-											  0,
-											  into->onCommit,
-											  reloptions,
-											  true,
-											  allowSystemTableMods);
-	Assert(intoRelationId != InvalidOid);
-
-	/*
-	 * Advance command counter so that the newly-created relation's catalog
-	 * tuples will be visible to heap_open.
-	 */
-	CommandCounterIncrement();
-
-	/*
-	 * If necessary, create a TOAST table for the INTO relation. Note that
-	 * AlterTableCreateToastTable ends with CommandCounterIncrement(), so that
-	 * the TOAST table will be visible for insertion.
-	 */
-	reloptions = transformRelOptions((Datum) 0,
-									 into->options,
-									 "toast",
-									 validnsps,
-									 true,
-									 false);
-
-	(void) heap_reloptions(RELKIND_TOASTVALUE, reloptions, true);
-
-	AlterTableCreateToastTable(intoRelationId, reloptions);
-
-	/*
-	 * And open the constructed table for writing.
-	 */
-	intoRelationDesc = heap_open(intoRelationId, AccessExclusiveLock);
-
-	/*
-	 * Check INSERT permission on the constructed table.
-	 */
-	rte = makeNode(RangeTblEntry);
-	rte->rtekind = RTE_RELATION;
-	rte->relid = intoRelationId;
-	rte->relkind = RELKIND_RELATION;
-	rte->requiredPerms = ACL_INSERT;
-
-	for (attnum = 1; attnum <= intoTupDesc->natts; attnum++)
-		rte->modifiedCols = bms_add_member(rte->modifiedCols,
-				attnum - FirstLowInvalidHeapAttributeNumber);
-
-	ExecCheckRTPerms(list_make1(rte), true);
-
-	/*
-	 * Now replace the query's DestReceiver with one for SELECT INTO
-	 */
-	queryDesc->dest = CreateDestReceiver(DestIntoRel);
-	myState = (DR_intorel *) queryDesc->dest;
-	Assert(myState->pub.mydest == DestIntoRel);
-	myState->estate = estate;
-	myState->rel = intoRelationDesc;
-
-	/*
-	 * We can skip WAL-logging the insertions, unless PITR or streaming
-	 * replication is in use. We can skip the FSM in any case.
-	 */
-	myState->hi_options = HEAP_INSERT_SKIP_FSM |
-		(XLogIsNeeded() ? 0 : HEAP_INSERT_SKIP_WAL);
-	myState->bistate = GetBulkInsertState();
-
-	/* Not using WAL requires smgr_targblock be initially invalid */
-	Assert(RelationGetTargetBlock(intoRelationDesc) == InvalidBlockNumber);
-}
-
-/*
- * CloseIntoRel --- clean up SELECT INTO at ExecutorEnd time
- */
-static void
-CloseIntoRel(QueryDesc *queryDesc)
-{
-	DR_intorel *myState = (DR_intorel *) queryDesc->dest;
-
-	/* OpenIntoRel might never have gotten called */
-	if (myState && myState->pub.mydest == DestIntoRel && myState->rel)
-	{
-		FreeBulkInsertState(myState->bistate);
-
-		/* If we skipped using WAL, must heap_sync before commit */
-		if (myState->hi_options & HEAP_INSERT_SKIP_WAL)
-			heap_sync(myState->rel);
-
-		/* close rel, but keep lock until commit */
-		heap_close(myState->rel, NoLock);
-
-		myState->rel = NULL;
-	}
-}
-
-/*
- * CreateIntoRelDestReceiver -- create a suitable DestReceiver object
- */
-DestReceiver *
-CreateIntoRelDestReceiver(void)
-{
-	DR_intorel *self = (DR_intorel *) palloc0(sizeof(DR_intorel));
-
-	self->pub.receiveSlot = intorel_receive;
-	self->pub.rStartup = intorel_startup;
-	self->pub.rShutdown = intorel_shutdown;
-	self->pub.rDestroy = intorel_destroy;
-	self->pub.mydest = DestIntoRel;
-
-	/* private fields will be set by OpenIntoRel */
-
-	return (DestReceiver *) self;
-}
-
-/*
- * intorel_startup --- executor startup
- */
-static void
-intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
-{
-	/* no-op */
-}
-
-/*
- * intorel_receive --- receive one tuple
- */
-static void
-intorel_receive(TupleTableSlot *slot, DestReceiver *self)
-{
-	DR_intorel *myState = (DR_intorel *) self;
-	HeapTuple	tuple;
-
-	/*
-	 * get the heap tuple out of the tuple table slot, making sure we have a
-	 * writable copy
-	 */
-	tuple = ExecMaterializeSlot(slot);
-
-	/*
-	 * force assignment of new OID (see comments in ExecInsert)
-	 */
-	if (myState->rel->rd_rel->relhasoids)
-		HeapTupleSetOid(tuple, InvalidOid);
-
-	heap_insert(myState->rel,
-				tuple,
-				myState->estate->es_output_cid,
-				myState->hi_options,
-				myState->bistate);
-
-	/* We know this is a newly created relation, so there are no indexes */
-}
-
-/*
- * intorel_shutdown --- executor end
- */
-static void
-intorel_shutdown(DestReceiver *self)
-{
-	/* no-op */
-}
-
-/*
- * intorel_destroy --- release DestReceiver object
- */
-static void
-intorel_destroy(DestReceiver *self)
-{
-	pfree(self);
-}
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index 65591e2..acab607 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -137,8 +137,6 @@ CreateExecutorState(void)
 
 	estate->es_top_eflags = 0;
 	estate->es_instrument = 0;
-	estate->es_select_into = false;
-	estate->es_into_oids = false;
 	estate->es_finished = false;
 
 	estate->es_exprcontexts = NIL;
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 45ca5ec..30185f2 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -349,7 +349,6 @@ init_execution_state(List *queryTree_list,
 
 			if (ps->commandType == CMD_SELECT &&
 				ps->utilityStmt == NULL &&
-				ps->intoClause == NULL &&
 				!ps->hasModifyingCTE)
 				fcache->lazyEval = lasttages->lazyEval = true;
 		}
@@ -1307,8 +1306,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
 	 */
 	if (parse &&
 		parse->commandType == CMD_SELECT &&
-		parse->utilityStmt == NULL &&
-		parse->intoClause == NULL)
+		parse->utilityStmt == NULL)
 	{
 		tlist_ptr = &parse->targetList;
 		tlist = parse->targetList;
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 688279c..d9f08ef 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -1921,7 +1921,11 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 				if (_SPI_current->tuptable)
 					_SPI_current->processed = _SPI_current->tuptable->alloced -
 						_SPI_current->tuptable->free;
-				res = SPI_OK_UTILITY;
+				if(nodeTag(stmt) == T_CreateTableAsStmt &&
+				   ((CreateTableAsStmt*)stmt)->is_select_into)
+					res = SPI_OK_SELINTO;
+				else
+					res = SPI_OK_UTILITY;
 			}
 
 			/*
@@ -2046,9 +2050,7 @@ _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, long tcount)
 	{
 		case CMD_SELECT:
 			Assert(queryDesc->plannedstmt->utilityStmt == NULL);
-			if (queryDesc->plannedstmt->intoClause)		/* select into table? */
-				res = SPI_OK_SELINTO;
-			else if (queryDesc->dest->mydest != DestSPI)
+			if (queryDesc->dest->mydest != DestSPI)
 			{
 				/* Don't return SPI_OK_SELECT if we're discarding result */
 				res = SPI_OK_UTILITY;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c70a5bd..d8e7eb3 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -85,7 +85,6 @@ _copyPlannedStmt(PlannedStmt *from)
 	COPY_NODE_FIELD(rtable);
 	COPY_NODE_FIELD(resultRelations);
 	COPY_NODE_FIELD(utilityStmt);
-	COPY_NODE_FIELD(intoClause);
 	COPY_NODE_FIELD(subplans);
 	COPY_BITMAPSET_FIELD(rewindPlanIDs);
 	COPY_NODE_FIELD(rowMarks);
@@ -2417,7 +2416,6 @@ _copyQuery(Query *from)
 	COPY_SCALAR_FIELD(canSetTag);
 	COPY_NODE_FIELD(utilityStmt);
 	COPY_SCALAR_FIELD(resultRelation);
-	COPY_NODE_FIELD(intoClause);
 	COPY_SCALAR_FIELD(hasAggs);
 	COPY_SCALAR_FIELD(hasWindowFuncs);
 	COPY_SCALAR_FIELD(hasSubLinks);
@@ -3201,6 +3199,18 @@ _copyExplainStmt(ExplainStmt *from)
 	return newnode;
 }
 
+static CreateTableAsStmt *
+_copyCreateTableAsStmt(CreateTableAsStmt *from)
+{
+	CreateTableAsStmt *newnode = makeNode(CreateTableAsStmt);
+
+	COPY_NODE_FIELD(query);
+	COPY_NODE_FIELD(into);
+	COPY_SCALAR_FIELD(is_select_into);
+
+	return newnode;
+}
+
 static CreateSeqStmt *
 _copyCreateSeqStmt(CreateSeqStmt *from)
 {
@@ -3608,7 +3618,6 @@ _copyExecuteStmt(ExecuteStmt *from)
 	ExecuteStmt *newnode = makeNode(ExecuteStmt);
 
 	COPY_STRING_FIELD(name);
-	COPY_NODE_FIELD(into);
 	COPY_NODE_FIELD(params);
 
 	return newnode;
@@ -4243,6 +4252,9 @@ copyObject(void *from)
 		case T_ExplainStmt:
 			retval = _copyExplainStmt(from);
 			break;
+		case T_CreateTableAsStmt:
+			retval = _copyCreateTableAsStmt(from);
+			break;
 		case T_CreateSeqStmt:
 			retval = _copyCreateSeqStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f490a7a..e090e38 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -900,7 +900,6 @@ _equalQuery(Query *a, Query *b)
 	COMPARE_SCALAR_FIELD(canSetTag);
 	COMPARE_NODE_FIELD(utilityStmt);
 	COMPARE_SCALAR_FIELD(resultRelation);
-	COMPARE_NODE_FIELD(intoClause);
 	COMPARE_SCALAR_FIELD(hasAggs);
 	COMPARE_SCALAR_FIELD(hasWindowFuncs);
 	COMPARE_SCALAR_FIELD(hasSubLinks);
@@ -1560,6 +1559,17 @@ _equalExplainStmt(ExplainStmt *a, ExplainStmt *b)
 	return true;
 }
 
+
+static bool
+_equalCreateTableAsStmt(CreateTableAsStmt *a, CreateTableAsStmt *b)
+{
+	COMPARE_NODE_FIELD(query);
+	COMPARE_NODE_FIELD(into);
+	COMPARE_SCALAR_FIELD(is_select_into);
+
+	return true;
+}
+
 static bool
 _equalCreateSeqStmt(CreateSeqStmt *a, CreateSeqStmt *b)
 {
@@ -1903,7 +1913,6 @@ static bool
 _equalExecuteStmt(ExecuteStmt *a, ExecuteStmt *b)
 {
 	COMPARE_STRING_FIELD(name);
-	COMPARE_NODE_FIELD(into);
 	COMPARE_NODE_FIELD(params);
 
 	return true;
@@ -2786,6 +2795,9 @@ equal(void *a, void *b)
 		case T_ExplainStmt:
 			retval = _equalExplainStmt(a, b);
 			break;
+		case T_CreateTableAsStmt:
+			retval = _equalCreateTableAsStmt(a, b);
+			break;
 		case T_CreateSeqStmt:
 			retval = _equalCreateSeqStmt(a, b);
 			break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 31af47f..997f5e2 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -248,7 +248,6 @@ _outPlannedStmt(StringInfo str, PlannedStmt *node)
 	WRITE_NODE_FIELD(rtable);
 	WRITE_NODE_FIELD(resultRelations);
 	WRITE_NODE_FIELD(utilityStmt);
-	WRITE_NODE_FIELD(intoClause);
 	WRITE_NODE_FIELD(subplans);
 	WRITE_BITMAPSET_FIELD(rewindPlanIDs);
 	WRITE_NODE_FIELD(rowMarks);
@@ -2197,7 +2196,6 @@ _outQuery(StringInfo str, Query *node)
 		appendStringInfo(str, " :utilityStmt <>");
 
 	WRITE_INT_FIELD(resultRelation);
-	WRITE_NODE_FIELD(intoClause);
 	WRITE_BOOL_FIELD(hasAggs);
 	WRITE_BOOL_FIELD(hasWindowFuncs);
 	WRITE_BOOL_FIELD(hasSubLinks);
@@ -2813,7 +2811,7 @@ _outNode(StringInfo str, void *obj)
 			case T_RangeVar:
 				_outRangeVar(str, obj);
 				break;
-			case T_IntoClause:
+			case T_IntoClause:/*XXX: This could be removed but dim would need to add it right back*/
 				_outIntoClause(str, obj);
 				break;
 			case T_Var:
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3de20ad..8a35c4c 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -198,7 +198,6 @@ _readQuery(void)
 	READ_BOOL_FIELD(canSetTag);
 	READ_NODE_FIELD(utilityStmt);
 	READ_INT_FIELD(resultRelation);
-	READ_NODE_FIELD(intoClause);
 	READ_BOOL_FIELD(hasAggs);
 	READ_BOOL_FIELD(hasWindowFuncs);
 	READ_BOOL_FIELD(hasSubLinks);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 5c18b72..93b7773 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -233,7 +233,6 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	result->rtable = glob->finalrtable;
 	result->resultRelations = glob->resultRelations;
 	result->utilityStmt = parse->utilityStmt;
-	result->intoClause = parse->intoClause;
 	result->subplans = glob->subplans;
 	result->rewindPlanIDs = glob->rewindPlanIDs;
 	result->rowMarks = glob->finalrowmarks;
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index e396520..a995fdf 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1399,7 +1399,6 @@ simplify_EXISTS_query(Query *query)
 	 * are complex.
 	 */
 	if (query->commandType != CMD_SELECT ||
-		query->intoClause ||
 		query->setOperations ||
 		query->hasAggs ||
 		query->hasWindowFuncs ||
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 8bb011b..f71a988 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1047,8 +1047,7 @@ is_simple_subquery(Query *subquery)
 	 */
 	if (!IsA(subquery, Query) ||
 		subquery->commandType != CMD_SELECT ||
-		subquery->utilityStmt != NULL ||
-		subquery->intoClause != NULL)
+		subquery->utilityStmt != NULL)
 		elog(ERROR, "subquery is bogus");
 
 	/*
@@ -1134,8 +1133,7 @@ is_simple_union_all(Query *subquery)
 	/* Let's just make sure it's a valid subselect ... */
 	if (!IsA(subquery, Query) ||
 		subquery->commandType != CMD_SELECT ||
-		subquery->utilityStmt != NULL ||
-		subquery->intoClause != NULL)
+		subquery->utilityStmt != NULL)
 		elog(ERROR, "subquery is bogus");
 
 	/* Is it a set-operation query at all? */
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index ad02950..7422387 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4028,7 +4028,6 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 	if (!IsA(querytree, Query) ||
 		querytree->commandType != CMD_SELECT ||
 		querytree->utilityStmt ||
-		querytree->intoClause ||
 		querytree->hasAggs ||
 		querytree->hasWindowFuncs ||
 		querytree->hasSubLinks ||
@@ -4542,8 +4541,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
 	 */
 	if (!IsA(querytree, Query) ||
 		querytree->commandType != CMD_SELECT ||
-		querytree->utilityStmt ||
-		querytree->intoClause)
+		querytree->utilityStmt)
 		goto fail;
 
 	/*
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index dae5478..1d17f8b 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -62,6 +62,8 @@ static Query *transformDeclareCursorStmt(ParseState *pstate,
 						   DeclareCursorStmt *stmt);
 static Query *transformExplainStmt(ParseState *pstate,
 					 ExplainStmt *stmt);
+static Query *transformCreateTableAsStmt(ParseState *pstate,
+					 CreateTableAsStmt *stmt);
 static void transformLockingClause(ParseState *pstate, Query *qry,
 					   LockingClause *lc, bool pushedDown);
 
@@ -202,6 +204,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
 										  (ExplainStmt *) parseTree);
 			break;
 
+		case T_CreateTableAsStmt:
+			result = transformCreateTableAsStmt(pstate,
+			                           (CreateTableAsStmt *) parseTree);
+			break;
+
 		default:
 
 			/*
@@ -257,6 +264,7 @@ analyze_requires_snapshot(Node *parseTree)
 			result = true;
 			break;
 
+		case T_CreateTableAsStmt:
 		case T_ExplainStmt:
 			/* yes, because we must analyze the contained statement */
 			result = true;
@@ -455,21 +463,29 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
 		sub_pstate->p_relnamespace = sub_relnamespace;
 		sub_pstate->p_varnamespace = sub_varnamespace;
 
+		/* The grammar should have produced a SELECT, but it might have INTO */
+		if(IsA(stmt->selectStmt, SelectStmt))
+		{
+			SelectStmt* first = (SelectStmt*)stmt->selectStmt;
+			while (first && first->op != SETOP_NONE)
+				first = first->larg;
+
+			if (first->intoClause)
+				ereport(ERROR,
+				        (errcode(ERRCODE_SYNTAX_ERROR),
+				         errmsg("INSERT ... SELECT cannot specify INTO"),
+				         parser_errposition(pstate,
+				                            exprLocation((Node *)first->intoClause))));
+		}
+
 		selectQuery = transformStmt(sub_pstate, stmt->selectStmt);
 
 		free_parsestate(sub_pstate);
 
-		/* The grammar should have produced a SELECT, but it might have INTO */
 		if (!IsA(selectQuery, Query) ||
 			selectQuery->commandType != CMD_SELECT ||
 			selectQuery->utilityStmt != NULL)
 			elog(ERROR, "unexpected non-SELECT command in INSERT ... SELECT");
-		if (selectQuery->intoClause)
-			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("INSERT ... SELECT cannot specify INTO"),
-					 parser_errposition(pstate,
-						   exprLocation((Node *) selectQuery->intoClause))));
 
 		/*
 		 * Make the source be a subquery in the INSERT's rangetable, and add
@@ -876,6 +892,18 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	Node	   *qual;
 	ListCell   *l;
 
+	/*
+	 * Validity-check whether we got called from somewhere where
+	 * ... INTO was not allowed
+	 */
+	if (stmt->intoClause)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("INTO is only allowed on first SELECT of UNION/INTERSECT/EXCEPT"),
+				 parser_errposition(pstate,
+				                    exprLocation((Node *) stmt->intoClause))));
+
+
 	qry->commandType = CMD_SELECT;
 
 	/* process the WITH clause independently of all else */
@@ -963,8 +991,6 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 												   pstate->p_windowdefs,
 												   &qry->targetList);
 
-	/* SELECT INTO/CREATE TABLE AS spec is just passed through */
-	qry->intoClause = stmt->intoClause;
 
 	qry->rtable = pstate->p_rtable;
 	qry->jointree = makeFromExpr(pstate->p_joinlist, qual);
@@ -1185,9 +1211,6 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 			 errmsg("SELECT FOR UPDATE/SHARE cannot be applied to VALUES")));
 
-	/* CREATE TABLE AS spec is just passed through */
-	qry->intoClause = stmt->intoClause;
-
 	/*
 	 * There mustn't have been any table references in the expressions, else
 	 * strange things would happen, like Cartesian products of those tables
@@ -1253,7 +1276,6 @@ static Query *
 transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
-	SelectStmt *leftmostSelect;
 	int			leftmostRTI;
 	Query	   *leftmostQuery;
 	SetOperationStmt *sostmt;
@@ -1286,20 +1308,6 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 	}
 
 	/*
-	 * Find leftmost leaf SelectStmt; extract the one-time-only items from it
-	 * and from the top-level node.
-	 */
-	leftmostSelect = stmt->larg;
-	while (leftmostSelect && leftmostSelect->op != SETOP_NONE)
-		leftmostSelect = leftmostSelect->larg;
-	Assert(leftmostSelect && IsA(leftmostSelect, SelectStmt) &&
-		   leftmostSelect->larg == NULL);
-	qry->intoClause = leftmostSelect->intoClause;
-
-	/* clear this to prevent complaints in transformSetOperationTree() */
-	leftmostSelect->intoClause = NULL;
-
-	/*
 	 * These are not one-time, exactly, but we want to process them here and
 	 * not let transformSetOperationTree() see them --- else it'll just
 	 * recurse right back here!
@@ -1330,7 +1338,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 	qry->setOperations = (Node *) sostmt;
 
 	/*
-	 * Re-find leftmost SELECT (now it's a sub-query in rangetable)
+	 * Find leftmost SELECT (it's a sub-query in rangetable)
 	 */
 	node = sostmt->larg;
 	while (node && IsA(node, SetOperationStmt))
@@ -2099,6 +2107,25 @@ transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt)
 				(errcode(ERRCODE_INVALID_CURSOR_DEFINITION),
 				 errmsg("cannot specify both SCROLL and NO SCROLL")));
 
+	/*
+	 * We must explicitly disallow DECLARE CURSOR ... SELECT INTO We
+	 * have to do this before transformStmt() as that will holler if
+	 * it ever finds a intoClause
+	 */
+	if(IsA(stmt->query, SelectStmt))
+	{
+		SelectStmt* first = (SelectStmt*)stmt->query;
+		while (first && first->op != SETOP_NONE)
+			first = first->larg;
+
+		if (first->intoClause)
+			ereport(ERROR,
+			        (errcode(ERRCODE_INVALID_CURSOR_DEFINITION),
+			         errmsg("DECLARE CURSOR cannot specify INTO"),
+			         parser_errposition(pstate,
+			                            exprLocation((Node *) first->intoClause))));
+	}
+
 	result = transformStmt(pstate, stmt->query);
 
 	/* Grammar should not have allowed anything but SELECT */
@@ -2107,14 +2134,6 @@ transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt)
 		result->utilityStmt != NULL)
 		elog(ERROR, "unexpected non-SELECT command in DECLARE CURSOR");
 
-	/* But we must explicitly disallow DECLARE CURSOR ... SELECT INTO */
-	if (result->intoClause)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_CURSOR_DEFINITION),
-				 errmsg("DECLARE CURSOR cannot specify INTO"),
-				 parser_errposition(pstate,
-								exprLocation((Node *) result->intoClause))));
-
 	/*
 	 * We also disallow data-modifying WITH in a cursor.  (This could be
 	 * allowed, but the semantics of when the updates occur might be
@@ -2183,6 +2202,28 @@ transformExplainStmt(ParseState *pstate, ExplainStmt *stmt)
 
 
 /*
+ * transformCreateTableAsStmt -
+ *	transform an CREATE TABLE AS/SELECT ... INTO Statement
+ *
+ */
+static Query *
+transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt)
+{
+	Query	   *result;
+
+	/* transform contained query */
+	stmt->query = (Node *) transformStmt(pstate, stmt->query);
+
+	/* represent the command as a utility Query */
+	result = makeNode(Query);
+	result->commandType = CMD_UTILITY;
+	result->utilityStmt = (Node *) stmt;
+
+	return result;
+}
+
+
+/*
  * Check for features that are not supported together with FOR UPDATE/SHARE.
  *
  * exported so planner can check again after rewriting, query pullup, etc
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2a497d1..fe1bddc 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -3010,23 +3010,28 @@ ExistingIndex:   USING INDEX index_name				{ $$ = $3; }
 CreateAsStmt:
 		CREATE OptTemp TABLE create_as_target AS SelectStmt opt_with_data
 				{
+                    SelectStmt *n;
+					CreateTableAsStmt* ctas = makeNode(CreateTableAsStmt);
+                    ctas->into = $4;
+                    ctas->query = $6;
+                    ctas->is_select_into = false;
+                    $4->skipData = !($7);
+					$4->rel->relpersistence = $2;
+					$4->skipData = !($7);
+                    $$ = (Node*)ctas;
+
 					/*
 					 * When the SelectStmt is a set-operation tree, we must
 					 * stuff the INTO information into the leftmost component
 					 * Select, because that's where analyze.c will expect
 					 * to find it.
 					 */
-					SelectStmt *n = findLeftmostSelect((SelectStmt *) $6);
+					n = findLeftmostSelect((SelectStmt *) $6);
 					if (n->intoClause != NULL)
 						ereport(ERROR,
 								(errcode(ERRCODE_SYNTAX_ERROR),
 								 errmsg("CREATE TABLE AS cannot specify INTO"),
 								 parser_errposition(exprLocation((Node *) n->intoClause))));
-					n->intoClause = $4;
-					/* cram additional flags into the IntoClause */
-					$4->rel->relpersistence = $2;
-					$4->skipData = !($7);
-					$$ = $6;
 				}
 		;
 
@@ -7994,20 +7999,25 @@ ExecuteStmt: EXECUTE name execute_param_clause
 					ExecuteStmt *n = makeNode(ExecuteStmt);
 					n->name = $2;
 					n->params = $3;
-					n->into = NULL;
 					$$ = (Node *) n;
 				}
 			| CREATE OptTemp TABLE create_as_target AS
 				EXECUTE name execute_param_clause opt_with_data
 				{
 					ExecuteStmt *n = makeNode(ExecuteStmt);
+					CreateTableAsStmt* ctas = makeNode(CreateTableAsStmt);
+
 					n->name = $7;
 					n->params = $8;
-					n->into = $4;
+
 					/* cram additional flags into the IntoClause */
 					$4->rel->relpersistence = $2;
 					$4->skipData = !($9);
-					$$ = (Node *) n;
+
+                    ctas->into = $4;
+                    ctas->query = (Node*)n;
+                    ctas->is_select_into = false;
+					$$ = (Node *) ctas;
 				}
 		;
 
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index e8177bc..4758835 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -481,6 +481,21 @@ transformRangeSubselect(ParseState *pstate, RangeSubselect *r)
 	if (r->alias == NULL)
 		elog(ERROR, "subquery in FROM must have an alias");
 
+
+	if(IsA(r->subquery, SelectStmt))
+	{
+		SelectStmt* first = (SelectStmt*)r->subquery;
+		while (first && first->op != SETOP_NONE)
+			first = first->larg;
+
+		if (first->intoClause)
+			ereport(ERROR,
+			        (errcode(ERRCODE_SYNTAX_ERROR),
+			         errmsg("subquery in FROM cannot have SELECT INTO"),
+			         parser_errposition(pstate,
+			                            exprLocation((Node *)first->intoClause))));
+	}
+
 	/*
 	 * Analyze and transform the subquery.
 	 */
@@ -495,12 +510,6 @@ transformRangeSubselect(ParseState *pstate, RangeSubselect *r)
 		query->commandType != CMD_SELECT ||
 		query->utilityStmt != NULL)
 		elog(ERROR, "unexpected non-SELECT command in subquery in FROM");
-	if (query->intoClause)
-		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("subquery in FROM cannot have SELECT INTO"),
-				 parser_errposition(pstate,
-								 exprLocation((Node *) query->intoClause))));
 
 	/*
 	 * The subquery cannot make use of any variables from FROM items created
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index ec6afd8..299e609 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -241,6 +241,20 @@ analyzeCTE(ParseState *pstate, CommonTableExpr *cte)
 	/* Analysis not done already */
 	Assert(!IsA(cte->ctequery, Query));
 
+	if(IsA(cte->ctequery, SelectStmt))
+	{
+		SelectStmt* first = (SelectStmt*)cte->ctequery;
+		while (first && first->op != SETOP_NONE)
+			first = first->larg;
+
+		if (first->intoClause)
+			ereport(ERROR,
+			        (errcode(ERRCODE_SYNTAX_ERROR),
+			         errmsg("subquery in WITH cannot have SELECT INTO"),
+			         parser_errposition(pstate,
+			                            exprLocation((Node *) first->intoClause))));
+	}
+
 	query = parse_sub_analyze(cte->ctequery, pstate, cte, false);
 	cte->ctequery = (Node *) query;
 
@@ -253,13 +267,6 @@ analyzeCTE(ParseState *pstate, CommonTableExpr *cte)
 	if (query->utilityStmt != NULL)
 		elog(ERROR, "unexpected utility statement in WITH");
 
-	if (query->intoClause)
-		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("subquery in WITH cannot have SELECT INTO"),
-				 parser_errposition(pstate,
-								 exprLocation((Node *) query->intoClause))));
-
 	/*
 	 * We disallow data-modifying WITH except at the top level of a query,
 	 * because it's not clear when such a modification should be executed.
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 75236c7..ff8b7c5 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1398,6 +1398,21 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		return result;
 
 	pstate->p_hasSubLinks = true;
+
+	if(IsA(sublink->subselect, SelectStmt))
+	{
+		SelectStmt* first = (SelectStmt*)sublink->subselect;
+		while (first && first->op != SETOP_NONE)
+			first = first->larg;
+
+		if (first->intoClause)
+			ereport(ERROR,
+			        (errcode(ERRCODE_SYNTAX_ERROR),
+			         errmsg("subquery cannot have SELECT INTO"),
+			         parser_errposition(pstate,
+			                            exprLocation((Node *) first->intoClause))));
+	}
+
 	qtree = parse_sub_analyze(sublink->subselect, pstate, NULL, false);
 
 	/*
@@ -1408,12 +1423,6 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		qtree->commandType != CMD_SELECT ||
 		qtree->utilityStmt != NULL)
 		elog(ERROR, "unexpected non-SELECT command in SubLink");
-	if (qtree->intoClause)
-		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("subquery cannot have SELECT INTO"),
-				 parser_errposition(pstate,
-								 exprLocation((Node *) qtree->intoClause))));
 
 	sublink->subselect = (Node *) qtree;
 
diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c
index e389208..de28d84 100644
--- a/src/backend/parser/parser.c
+++ b/src/backend/parser/parser.c
@@ -37,6 +37,7 @@ raw_parser(const char *str)
 	core_yyscan_t yyscanner;
 	base_yy_extra_type yyextra;
 	int			yyresult;
+	ListCell *c;
 
 	/* initialize the flex scanner */
 	yyscanner = scanner_init(str, &yyextra.core_yy_extra,
@@ -57,6 +58,43 @@ raw_parser(const char *str)
 	if (yyresult)				/* error */
 		return NIL;
 
+	/*
+	 * Some things are rather hard to properly diagnose in grammar
+	 * without complicating/duplicating it too much. So we do some
+	 * postprocessing here.
+	 */
+	foreach(c, yyextra.parsetree){
+		switch(nodeTag(lfirst(c))){
+			/*
+			 * The grammar currently doesn't disambiguate between
+			 * SELECT and SELECT ... INTO. Do that now.
+			 */
+			case T_SelectStmt:
+			{
+				SelectStmt* s = (SelectStmt*)lfirst(c);
+				SelectStmt* first = s;
+				while (first && first->op != SETOP_NONE)
+					first = first->larg;
+				Assert(first && IsA(first, SelectStmt) && first->larg == NULL);
+				if(first->intoClause){
+					CreateTableAsStmt* ctas = makeNode(CreateTableAsStmt);
+					ctas->into = first->intoClause;
+					ctas->query = (Node*)s;
+					ctas->is_select_into = true;
+					/*
+					 * this way everyone can complain if this is set
+					 * without further checks because it shall never
+					 * be set but here.
+					 */
+					first->intoClause = NULL;
+					lfirst(c) = ctas;
+					break;
+				}
+			}
+			default:
+				break;
+		}
+	}
 	return yyextra.parsetree;
 }
 
diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c
index 17db70e..e359e9a 100644
--- a/src/backend/rewrite/rewriteDefine.c
+++ b/src/backend/rewrite/rewriteDefine.c
@@ -324,8 +324,7 @@ DefineQueryRewrite(char *rulename,
 		query = (Query *) linitial(action);
 		if (!is_instead ||
 			query->commandType != CMD_SELECT ||
-			query->utilityStmt != NULL ||
-			query->intoClause != NULL)
+			query->utilityStmt != NULL)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("rules on SELECT must have action INSTEAD SELECT")));
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 466727b..2628e31 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -260,8 +260,7 @@ ChoosePortalStrategy(List *stmts)
 			if (query->canSetTag)
 			{
 				if (query->commandType == CMD_SELECT &&
-					query->utilityStmt == NULL &&
-					query->intoClause == NULL)
+					query->utilityStmt == NULL)
 				{
 					if (query->hasModifyingCTE)
 						return PORTAL_ONE_MOD_WITH;
@@ -285,8 +284,7 @@ ChoosePortalStrategy(List *stmts)
 			if (pstmt->canSetTag)
 			{
 				if (pstmt->commandType == CMD_SELECT &&
-					pstmt->utilityStmt == NULL &&
-					pstmt->intoClause == NULL)
+					pstmt->utilityStmt == NULL)
 				{
 					if (pstmt->hasModifyingCTE)
 						return PORTAL_ONE_MOD_WITH;
@@ -395,8 +393,7 @@ FetchStatementTargetList(Node *stmt)
 		else
 		{
 			if (query->commandType == CMD_SELECT &&
-				query->utilityStmt == NULL &&
-				query->intoClause == NULL)
+				query->utilityStmt == NULL)
 				return query->targetList;
 			if (query->returningList)
 				return query->returningList;
@@ -408,8 +405,7 @@ FetchStatementTargetList(Node *stmt)
 		PlannedStmt *pstmt = (PlannedStmt *) stmt;
 
 		if (pstmt->commandType == CMD_SELECT &&
-			pstmt->utilityStmt == NULL &&
-			pstmt->intoClause == NULL)
+			pstmt->utilityStmt == NULL)
 			return pstmt->planTree->targetlist;
 		if (pstmt->hasReturning)
 			return pstmt->planTree->targetlist;
@@ -430,7 +426,6 @@ FetchStatementTargetList(Node *stmt)
 		ExecuteStmt *estmt = (ExecuteStmt *) stmt;
 		PreparedStatement *entry;
 
-		Assert(!estmt->into);
 		entry = FetchPreparedStatement(estmt->name, true);
 		return FetchPreparedStatementTargetList(entry);
 	}
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 6f88c47..4c5be0a 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -127,9 +127,7 @@ CommandIsReadOnly(Node *parsetree)
 		switch (stmt->commandType)
 		{
 			case CMD_SELECT:
-				if (stmt->intoClause != NULL)
-					return false;		/* SELECT INTO */
-				else if (stmt->rowMarks != NIL)
+				if (stmt->rowMarks != NIL)
 					return false;		/* SELECT FOR UPDATE/SHARE */
 				else if (stmt->hasModifyingCTE)
 					return false;		/* data-modifying CTE */
@@ -198,6 +196,7 @@ check_xact_readonly(Node *parsetree)
 		case T_CreateSchemaStmt:
 		case T_CreateSeqStmt:
 		case T_CreateStmt:
+		case T_CreateTableAsStmt:
 		case T_CreateTableSpaceStmt:
 		case T_CreateTrigStmt:
 		case T_CompositeTypeStmt:
@@ -1017,6 +1016,10 @@ standard_ProcessUtility(Node *parsetree,
 			ExplainQuery((ExplainStmt *) parsetree, queryString, params, dest);
 			break;
 
+		case T_CreateTableAsStmt:
+			CreateTableAs((CreateTableAsStmt *) parsetree, queryString, params, dest);
+			break;
+
 		case T_VariableSetStmt:
 			ExecSetVariableStmt((VariableSetStmt *) parsetree);
 			break;
@@ -1211,8 +1214,6 @@ UtilityReturnsTuples(Node *parsetree)
 				ExecuteStmt *stmt = (ExecuteStmt *) parsetree;
 				PreparedStatement *entry;
 
-				if (stmt->into)
-					return false;
 				entry = FetchPreparedStatement(stmt->name, false);
 				if (!entry)
 					return false;		/* not our business to raise error */
@@ -1263,8 +1264,6 @@ UtilityTupleDescriptor(Node *parsetree)
 				ExecuteStmt *stmt = (ExecuteStmt *) parsetree;
 				PreparedStatement *entry;
 
-				if (stmt->into)
-					return NULL;
 				entry = FetchPreparedStatement(stmt->name, false);
 				if (!entry)
 					return NULL;	/* not our business to raise error */
@@ -1299,8 +1298,7 @@ QueryReturnsTuples(Query *parsetree)
 	{
 		case CMD_SELECT:
 			/* returns tuples ... unless it's DECLARE CURSOR or SELECT INTO */
-			if (parsetree->utilityStmt == NULL &&
-				parsetree->intoClause == NULL)
+			if (parsetree->utilityStmt == NULL)
 				return true;
 			break;
 		case CMD_INSERT:
@@ -1888,6 +1886,13 @@ CreateCommandTag(Node *parsetree)
 			tag = "EXPLAIN";
 			break;
 
+		case T_CreateTableAsStmt:
+			if (((CreateTableAsStmt*)parsetree)->is_select_into)
+				tag = "SELECT INTO";
+			else
+				tag = "CREATE TABLE AS";
+			break;
+
 		case T_VariableSetStmt:
 			switch (((VariableSetStmt *) parsetree)->kind)
 			{
@@ -2041,8 +2046,6 @@ CreateCommandTag(Node *parsetree)
 							Assert(IsA(stmt->utilityStmt, DeclareCursorStmt));
 							tag = "DECLARE CURSOR";
 						}
-						else if (stmt->intoClause != NULL)
-							tag = "SELECT INTO";
 						else if (stmt->rowMarks != NIL)
 						{
 							/* not 100% but probably close enough */
@@ -2091,8 +2094,6 @@ CreateCommandTag(Node *parsetree)
 							Assert(IsA(stmt->utilityStmt, DeclareCursorStmt));
 							tag = "DECLARE CURSOR";
 						}
-						else if (stmt->intoClause != NULL)
-							tag = "SELECT INTO";
 						else if (stmt->rowMarks != NIL)
 						{
 							/* not 100% but probably close enough */
@@ -2159,10 +2160,7 @@ GetCommandLogLevel(Node *parsetree)
 			break;
 
 		case T_SelectStmt:
-			if (((SelectStmt *) parsetree)->intoClause)
-				lev = LOGSTMT_DDL;		/* CREATE AS, SELECT INTO */
-			else
-				lev = LOGSTMT_ALL;
+			lev = LOGSTMT_ALL;
 			break;
 
 			/* utility statements --- same whether raw or cooked */
@@ -2410,6 +2408,10 @@ GetCommandLogLevel(Node *parsetree)
 			}
 			break;
 
+		case T_CreateTableAsStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		case T_VariableSetStmt:
 			lev = LOGSTMT_ALL;
 			break;
@@ -2510,10 +2512,7 @@ GetCommandLogLevel(Node *parsetree)
 				switch (stmt->commandType)
 				{
 					case CMD_SELECT:
-						if (stmt->intoClause != NULL)
-							lev = LOGSTMT_DDL;	/* CREATE AS, SELECT INTO */
-						else
-							lev = LOGSTMT_ALL;	/* SELECT or DECLARE CURSOR */
+						lev = LOGSTMT_ALL;
 						break;
 
 					case CMD_UPDATE:
@@ -2539,10 +2538,7 @@ GetCommandLogLevel(Node *parsetree)
 				switch (stmt->commandType)
 				{
 					case CMD_SELECT:
-						if (stmt->intoClause != NULL)
-							lev = LOGSTMT_DDL;	/* CREATE AS, SELECT INTO */
-						else
-							lev = LOGSTMT_ALL;	/* SELECT or DECLARE CURSOR */
+						lev = LOGSTMT_ALL;
 						break;
 
 					case CMD_UPDATE:
diff --git a/src/include/access/htup.h b/src/include/access/htup.h
index 3ca25ac..01e3049 100644
--- a/src/include/access/htup.h
+++ b/src/include/access/htup.h
@@ -311,7 +311,7 @@ do { \
 
 #define HeapTupleHeaderSetOid(tup, oid) \
 do { \
-	Assert((tup)->t_infomask & HEAP_HASOID); \
+	if(!((tup)->t_infomask & HEAP_HASOID)) elog(ERROR, "HEAP_HASOID"); \
 	*((Oid *) ((char *)(tup) + (tup)->t_hoff - sizeof(Oid))) = (oid); \
 } while (0)
 
diff --git a/src/include/commands/prepare.h b/src/include/commands/prepare.h
index 52362fa..e0c2571 100644
--- a/src/include/commands/prepare.h
+++ b/src/include/commands/prepare.h
@@ -54,4 +54,7 @@ extern List *FetchPreparedStatementTargetList(PreparedStatement *stmt);
 
 void		DropAllPreparedStatements(void);
 
+extern ParamListInfo EvaluateParams(PreparedStatement *pstmt, List *params,
+                                    const char *queryString, EState *estate);
+
 #endif   /* PREPARE_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 333e303..bac53ac 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -15,6 +15,7 @@
 #define TABLECMDS_H
 
 #include "access/htup.h"
+#include "executor/executor.h"
 #include "nodes/parsenodes.h"
 #include "storage/lock.h"
 #include "utils/relcache.h"
@@ -22,6 +23,9 @@
 
 extern Oid	DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId);
 
+extern void CreateTableAs(CreateTableAsStmt *stmt, const char *queryString,
+			 ParamListInfo params, DestReceiver *dest);
+
 extern void RemoveRelations(DropStmt *drop);
 
 extern void AlterTable(AlterTableStmt *stmt);
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index bdd499b..a029984 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -49,12 +49,17 @@
  * AfterTriggerBeginQuery/AfterTriggerEndQuery.  This does not necessarily
  * mean that the plan can't queue any AFTER triggers; just that the caller
  * is responsible for there being a trigger context for them to be queued in.
+ *
+ * FORCE_OIDS tells the executor to build a tupleDesc that includes
+ * the oid column. This is used from tablecmd.c's CreateTableAs.
  */
 #define EXEC_FLAG_EXPLAIN_ONLY	0x0001	/* EXPLAIN, no ANALYZE */
 #define EXEC_FLAG_REWIND		0x0002	/* need efficient rescan */
 #define EXEC_FLAG_BACKWARD		0x0004	/* need backward scan */
 #define EXEC_FLAG_MARK			0x0008	/* need mark/restore */
 #define EXEC_FLAG_SKIP_TRIGGERS 0x0010	/* skip AfterTrigger calls */
+#define EXEC_FLAG_FORCE_OIDS_ON 0x0020	/* force oids in returned tuples */
+#define EXEC_FLAG_FORCE_OIDS_OFF 0x0040	/* force oids in returned tuples */
 
 
 /*
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0a89f18..4f9c569 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -321,6 +321,12 @@ typedef struct ResultRelInfo
 	ProjectionInfo *ri_projectReturning;
 } ResultRelInfo;
 
+typedef enum {
+	ESTATE_FORCE_OIDS_DEFAULT,
+	ESTATE_FORCE_OIDS_ON,
+	ESTATE_FORCE_OIDS_OFF
+} EStateForceOids;
+
 /* ----------------
  *	  EState information
  *
@@ -370,8 +376,7 @@ typedef struct EState
 
 	int			es_top_eflags;	/* eflags passed to ExecutorStart */
 	int			es_instrument;	/* OR of InstrumentOption flags */
-	bool		es_select_into; /* true if doing SELECT INTO */
-	bool		es_into_oids;	/* true to generate OIDs in SELECT INTO */
+	EStateForceOids        es_force_oids;  /* used by CreateTableAs */
 	bool		es_finished;	/* true when ExecutorFinish is done */
 
 	List	   *es_exprcontexts;	/* List of ExprContexts within EState */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 3a24089..3f48b3f 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -305,6 +305,7 @@ typedef enum NodeTag
 	T_DropdbStmt,
 	T_VacuumStmt,
 	T_ExplainStmt,
+	T_CreateTableAsStmt,
 	T_CreateSeqStmt,
 	T_AlterSeqStmt,
 	T_VariableSetStmt,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9e277c5..6577660 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -111,8 +111,6 @@ typedef struct Query
 	int			resultRelation; /* rtable index of target relation for
 								 * INSERT/UPDATE/DELETE; 0 for SELECT */
 
-	IntoClause *intoClause;		/* target for SELECT INTO / CREATE TABLE AS */
-
 	bool		hasAggs;		/* has aggregates in tlist or havingQual */
 	bool		hasWindowFuncs; /* has window functions in tlist */
 	bool		hasSubLinks;	/* has subquery SubLink */
@@ -1008,7 +1006,7 @@ typedef struct SelectStmt
 	 */
 	List	   *distinctClause; /* NULL, list of DISTINCT ON exprs, or
 								 * lcons(NIL,NIL) for all (SELECT DISTINCT) */
-	IntoClause *intoClause;		/* target for SELECT INTO / CREATE TABLE AS */
+	IntoClause *intoClause;		/* target for SELECT INTO */
 	List	   *targetList;		/* the target list (of ResTarget) */
 	List	   *fromClause;		/* the FROM clause */
 	Node	   *whereClause;	/* WHERE qualification */
@@ -2373,7 +2371,7 @@ typedef struct VacuumStmt
  *		Explain Statement
  *
  * The "query" field is either a raw parse tree (SelectStmt, InsertStmt, etc)
- * or a Query node if parse analysis has been done.  Note that rewriting and
+ * or a Query node if parse analysis has been done. Note that rewriting and
  * planning of the query are always postponed until execution of EXPLAIN.
  * ----------------------
  */
@@ -2385,6 +2383,19 @@ typedef struct ExplainStmt
 } ExplainStmt;
 
 /* ----------------------
+ * analyzing, rewriting and planning are handled as in explain
+ * statements (see comment above) only that query can only be a
+ * SelectStmt and not some other type.
+ */
+typedef struct CreateTableAsStmt
+{
+	NodeTag		type;
+	IntoClause  *into;
+	Node        *query;			/* the query (see comments above) */
+	bool        is_select_into; /* plpgsql wants to disambiguate */
+} CreateTableAsStmt;
+
+/* ----------------------
  * Checkpoint Statement
  * ----------------------
  */
@@ -2498,7 +2509,6 @@ typedef struct ExecuteStmt
 {
 	NodeTag		type;
 	char	   *name;			/* The name of the plan to execute */
-	IntoClause *into;			/* Optional table to store results in */
 	List	   *params;			/* Values to assign to parameters */
 } ExecuteStmt;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 6685864..acaae3e 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -54,8 +54,6 @@ typedef struct PlannedStmt
 
 	Node	   *utilityStmt;	/* non-null if this is DECLARE CURSOR */
 
-	IntoClause *intoClause;		/* target for SELECT INTO / CREATE TABLE AS */
-
 	List	   *subplans;		/* Plan trees for SubPlan expressions */
 
 	Bitmapset  *rewindPlanIDs;	/* indices of subplans that require REWIND */
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 717ad79..eee27d3 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -3271,29 +3271,18 @@ exec_stmt_dynexecute(PLpgSQL_execstate *estate,
 			break;
 
 		case SPI_OK_SELINTO:
-
 			/*
 			 * We want to disallow SELECT INTO for now, because its behavior
 			 * is not consistent with SELECT INTO in a normal plpgsql context.
 			 * (We need to reimplement EXECUTE to parse the string as a
 			 * plpgsql command, not just feed it to SPI_execute.) However,
-			 * CREATE AS should be allowed ... and since it produces the same
-			 * parsetree as SELECT INTO, there's no way to tell the difference
-			 * except to look at the source text.  Wotta kluge!
+			 * CREATE AS should be allowed ...
 			 */
-			{
-				char	   *ptr;
-
-				for (ptr = querystr; *ptr; ptr++)
-					if (!scanner_isspace(*ptr))
-						break;
-				if (*ptr == 'S' || *ptr == 's')
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("EXECUTE of SELECT ... INTO is not implemented"),
-							 errhint("You might want to use EXECUTE ... INTO or EXECUTE CREATE TABLE ... AS instead.")));
-				break;
-			}
+			ereport(ERROR,
+			        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			         errmsg("EXECUTE of SELECT ... INTO is not implemented"),
+			         errhint("You might want to use EXECUTE ... INTO or EXECUTE CREATE TABLE ... AS instead.")));
+			break;
 
 			/* Some SPI errors deserve specific error messages */
 		case SPI_ERROR_COPY:
@@ -5733,7 +5722,7 @@ exec_simple_check_plan(PLpgSQL_expr *expr)
 	 */
 	if (!IsA(query, Query))
 		return;
-	if (query->commandType != CMD_SELECT || query->intoClause)
+	if (query->commandType != CMD_SELECT)
 		return;
 	if (query->rtable != NIL)
 		return;
@@ -5807,7 +5796,7 @@ exec_simple_recheck_plan(PLpgSQL_expr *expr, CachedPlan *cplan)
 	 */
 	if (!IsA(stmt, PlannedStmt))
 		return;
-	if (stmt->commandType != CMD_SELECT || stmt->intoClause)
+	if (stmt->commandType != CMD_SELECT)
 		return;
 	plan = stmt->planTree;
 	if (!IsA(plan, Result))
diff --git a/src/test/regress/expected/select_into.out b/src/test/regress/expected/select_into.out
index 9ed4229..20bbc16 100644
--- a/src/test/regress/expected/select_into.out
+++ b/src/test/regress/expected/select_into.out
@@ -44,6 +44,25 @@ CREATE TABLE selinto_schema.tmp3 (a,b,c)
 	   AS SELECT oid,relname,relacl FROM pg_class
 	   WHERE relname like '%c%';	-- OK
 RESET SESSION AUTHORIZATION;
+--
+-- disallowed uses of SELECT ... INTO. All should fail
+--
+DECLARE foo CURSOR FOR SELECT 1 INTO b;
+ERROR:  DECLARE CURSOR cannot specify INTO
+LINE 1: DECLARE foo CURSOR FOR SELECT 1 INTO b;
+                                             ^
+COPY (SELECT 1 INTO frak UNION SELECT 2) TO 'blub';
+ERROR:  COPY (SELECT INTO) is not supported
+SELECT * FROM (SELECT 1 INTO f) bar;
+ERROR:  subquery in FROM cannot have SELECT INTO
+LINE 1: SELECT * FROM (SELECT 1 INTO f) bar;
+                                     ^
+CREATE VIEW foo AS SELECT 1 INTO b;
+ERROR:  views must not contain SELECT INTO
+INSERT INTO b SELECT 1 INTO f;
+ERROR:  INSERT ... SELECT cannot specify INTO
+LINE 1: INSERT INTO b SELECT 1 INTO f;
+                                    ^
 DROP SCHEMA selinto_schema CASCADE;
 NOTICE:  drop cascades to 3 other objects
 DETAIL:  drop cascades to table selinto_schema.tmp1
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index f49ec0e..2da50cf 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -139,7 +139,7 @@ SELECT * FROM writetest, temptest; -- ok
 (0 rows)
 
 CREATE TABLE test AS SELECT * FROM writetest; -- fail
-ERROR:  cannot execute SELECT INTO in a read-only transaction
+ERROR:  cannot execute CREATE TABLE AS in a read-only transaction
 START TRANSACTION READ WRITE;
 DROP TABLE writetest; -- ok
 COMMIT;
diff --git a/src/test/regress/sql/select_into.sql b/src/test/regress/sql/select_into.sql
index 039d35c..f04fd7a 100644
--- a/src/test/regress/sql/select_into.sql
+++ b/src/test/regress/sql/select_into.sql
@@ -50,5 +50,20 @@ CREATE TABLE selinto_schema.tmp3 (a,b,c)
 	   WHERE relname like '%c%';	-- OK
 RESET SESSION AUTHORIZATION;
 
+
+--
+-- disallowed uses of SELECT ... INTO. All should fail
+--
+DECLARE foo CURSOR FOR SELECT 1 INTO b;
+
+COPY (SELECT 1 INTO frak UNION SELECT 2) TO 'blub';
+
+SELECT * FROM (SELECT 1 INTO f) bar;
+
+CREATE VIEW foo AS SELECT 1 INTO b;
+
+INSERT INTO b SELECT 1 INTO f;
+
+
 DROP SCHEMA selinto_schema CASCADE;
 DROP USER selinto_user;
-- 
1.7.6.409.ge7a85.dirty

#28Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#25)
Re: Command Triggers

On Sun, Dec 11, 2011 at 1:55 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Andres Freund <andres@anarazel.de> writes:

Hm. I just noticed a relatively big hole in the patch: The handling of
deletion of dependent objects currently is nonexistant because they don't go
through ProcessUtility...

That's the reason why we're talking about “command triggers” rather than
“DDL triggers”.  We don't intend to fire the triggers at each DDL
operation happening on the server, but for each command.

This restriction still allows us to provide a very useful feature when
checked against the main use cases we target here. Those are auditing,
and replication (the replay will also CASCADEs), and a generic enough
SUDO facility (because the trigger function can well be SECURITY
DEFINER).

I haven't yet thought about your specific proposal here in enough to
have a fully-formed opinion, but I am a little nervous that this may
turn out to be one of those cases where the obvious API ends up
working less well than might have been supposed. For example,
consider the "auditing" use case, and suppose that the user wants to
log a message (to a table, or some other separate logging facility)
every time someone creates an index. In order to do that, they're
going to need to trap not only CREATE INDEX but also CREATE TABLE and
ALTER CONSTRAINT, and in the latter two cases they're then going to
have to then write some grotty logic to grovel through the statement
or its node tree in order to figure out what indexes are being
created. Also, if they want to log the name of the new index in cases
where the user doesn't specify it, they're going to have to duplicate
the backend logic which generates the index name, which is going to be
a pain in the ass and a recipe for future bugs (e.g. because we might
change the algorithm at some point, or the trigger might see a
different view of the world than the actual command, due to
intervening DDL).

This is something that has come up a lot in the context of sepgsql:
it's not really enough to know what the toplevel command is in some
cases, because whether or not the operation is allowed depends on
exactly what it's doing, which depends on things like which schema
gets used to create the table, and which tablespaces get used to
create the table and its indexes, and perhaps (given Peter's pending
work) what types are involved. You can deduce all of these things
from the command text, but it requires duplicating nontrivial amounts
of backend code, which is undesirable from a maintainability point of
view. I think the same issues are likely to arise in the context of
auditing, as in the example above, and even for replication: if, for
example, the index name that is chosen on the master happens to exist
on the standby with a different definition, replaying the actual
command text might succeed or fail depending on how the command is
phrased. Maybe it's OK for the slave to just choose a different name
for that index, but now a subsequent DROP INDEX command that gets
replayed across all servers might end up dropping logically unrelated
indexes on different machines.

Now, on the other hand, the conceptual simplicity of this approach is
very appealing, and I can certainly see people who would never
consider writing a ProcessUtility_hook using this type of facility.
However, I'm worried that it will be difficult to use as a foundation
for robust, general-purpose tools. Is that enough reason to reject
the whole approach? I'm not sure.

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

#29Alvaro Herrera
alvherre@commandprompt.com
In reply to: Robert Haas (#28)
Re: Command Triggers

Excerpts from Robert Haas's message of lun dic 12 13:32:45 -0300 2011:

On Sun, Dec 11, 2011 at 1:55 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Andres Freund <andres@anarazel.de> writes:

Hm. I just noticed a relatively big hole in the patch: The handling of
deletion of dependent objects currently is nonexistant because they don't go
through ProcessUtility...

That's the reason why we're talking about “command triggers” rather than
“DDL triggers”.  We don't intend to fire the triggers at each DDL
operation happening on the server, but for each command.

This restriction still allows us to provide a very useful feature when
checked against the main use cases we target here. Those are auditing,
and replication (the replay will also CASCADEs), and a generic enough
SUDO facility (because the trigger function can well be SECURITY
DEFINER).

I haven't yet thought about your specific proposal here in enough to
have a fully-formed opinion, but I am a little nervous that this may
turn out to be one of those cases where the obvious API ends up
working less well than might have been supposed. For example,
consider the "auditing" use case, and suppose that the user wants to
log a message (to a table, or some other separate logging facility)
every time someone creates an index. In order to do that, they're
going to need to trap not only CREATE INDEX but also CREATE TABLE and
ALTER CONSTRAINT,

Yeah. I remember mentioning the ability of CREATE SCHEMA to embed all
sort of object creation commands in a single top-level command, and
being handwaved away by Jan. "Nobody knows about that", I was told.
However, if this is to work in a robust way, these things should not be
ignored. In particular, firing triggers on each top-level command _is_
going to get some objects ignored; the people writing the triggers _are_
going to forget to handle CREATE SCHEMA properly most of the time.

--
Álvaro Herrera <alvherre@commandprompt.com>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support

#30Andres Freund
andres@anarazel.de
In reply to: Robert Haas (#28)
Re: Command Triggers

On Monday, December 12, 2011 05:32:45 PM Robert Haas wrote:

On Sun, Dec 11, 2011 at 1:55 PM, Dimitri Fontaine

<dimitri@2ndquadrant.fr> wrote:

Andres Freund <andres@anarazel.de> writes:

Hm. I just noticed a relatively big hole in the patch: The handling of
deletion of dependent objects currently is nonexistant because they
don't go through ProcessUtility...

That's the reason why we're talking about “command triggers” rather than
“DDL triggers”. We don't intend to fire the triggers at each DDL
operation happening on the server, but for each command.

This restriction still allows us to provide a very useful feature when
checked against the main use cases we target here. Those are auditing,
and replication (the replay will also CASCADEs), and a generic enough
SUDO facility (because the trigger function can well be SECURITY
DEFINER).

I haven't yet thought about your specific proposal here in enough to
have a fully-formed opinion, but I am a little nervous that this may
turn out to be one of those cases where the obvious API ends up
working less well than might have been supposed.

What are your thoughts about a "not-obvious api"?

For example,
consider the "auditing" use case, and suppose that the user wants to
log a message (to a table, or some other separate logging facility)
every time someone creates an index. In order to do that, they're
going to need to trap not only CREATE INDEX but also CREATE TABLE and
ALTER CONSTRAINT, and in the latter two cases they're then going to
have to then write some grotty logic to grovel through the statement
or its node tree in order to figure out what indexes are being
created. Also, if they want to log the name of the new index in cases
where the user doesn't specify it, they're going to have to duplicate
the backend logic which generates the index name, which is going to be
a pain in the ass and a recipe for future bugs (e.g. because we might
change the algorithm at some point, or the trigger might see a
different view of the world than the actual command, due to
intervening DDL).

I thought about using transformed statements before passing them to the
triggers to avoid exactly that issue. That would make stuff like CREATE TABLE
blub (LIKE), constraints and such transparent. It would mean quite some
additional effort though (because for several statements the transformation is
not really separated).

This is something that has come up a lot in the context of sepgsql:
it's not really enough to know what the toplevel command is in some
cases, because whether or not the operation is allowed depends on
exactly what it's doing, which depends on things like which schema
gets used to create the table, and which tablespaces get used to
create the table and its indexes, and perhaps (given Peter's pending
work) what types are involved. You can deduce all of these things
from the command text, but it requires duplicating nontrivial amounts
of backend code, which is undesirable from a maintainability point of
view. I think the same issues are likely to arise in the context of
auditing, as in the example above, and even for replication: if, for
example, the index name that is chosen on the master happens to exist
on the standby with a different definition, replaying the actual
command text might succeed or fail depending on how the command is
phrased. Maybe it's OK for the slave to just choose a different name
for that index, but now a subsequent DROP INDEX command that gets
replayed across all servers might end up dropping logically unrelated
indexes on different machines.

Well, you shouldn't grovel through the text - you do get passed the parsetree
which should make accessing that far easier. I can imagine somebody writing
some shiny layer which lets you grovel trough that with some xpath alike api.

Now, on the other hand, the conceptual simplicity of this approach is
very appealing, and I can certainly see people who would never
consider writing a ProcessUtility_hook using this type of facility.
However, I'm worried that it will be difficult to use as a foundation
for robust, general-purpose tools. Is that enough reason to reject
the whole approach? I'm not sure.

Thats where I am a bit unsure as well. I think the basic approach is sound but
it might need some work in several areas (transformed statements,
dependencies, ...). On the other hand: perfect is the enemy of good...

Having looked at it I don't think dependency handling is that much effort.
Havent looked at separation of the transformation phase.

Andres

#31Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Alvaro Herrera (#29)
Re: Command Triggers

Alvaro Herrera <alvherre@commandprompt.com> writes:

Yeah. I remember mentioning the ability of CREATE SCHEMA to embed all
sort of object creation commands in a single top-level command, and
being handwaved away by Jan. "Nobody knows about that", I was told.

In fact CREATE SCHEMA implementation will fire a process utility command
per element, see src/backend/commands/schemacmds.c:122

* Execute each command contained in the CREATE SCHEMA. Since the grammar
* allows only utility commands in CREATE SCHEMA, there is no need to pass
* them through parse_analyze() or the rewriter; we can just hand them
* straight to ProcessUtility.

However, if this is to work in a robust way, these things should not be
ignored. In particular, firing triggers on each top-level command _is_
going to get some objects ignored; the people writing the triggers _are_
going to forget to handle CREATE SCHEMA properly most of the time.

My current thinking is that the tool as proposed in the patch is useful
enough now already, and that we should cover its limitations in the
documentation. And also be somehow verbose about the nodeToString()
output where to find almost all you need.

Down the road, we might decide that either what we've done is covering
enough needs (after all, we managed not to have the features for years)
or decide to have internal commands implementation (create index from
inside a create table statement, say) go through ProcessUtility.

Also note that currently, anyone motivated enough to write a
ProcessUtility hook (in C) will face the same problem here, the hook
will not get called for any event where the command trigger would not
get called either.

So while I understand your concerns here, I think they are rather
theoretical, as we can already add support for create table, alter
table, and drop table to slony, londiste and bucardo with the existing
patch, and also implement create extension whitelisting and allow
non-superuser to install selected C coded extensions.

FWIW, those are the two main use cases I'm after.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#32Robert Haas
robertmhaas@gmail.com
In reply to: Andres Freund (#30)
Re: Command Triggers

On Mon, Dec 12, 2011 at 11:49 AM, Andres Freund <andres@anarazel.de> wrote:

I haven't yet thought about your specific proposal here in enough to
have a fully-formed opinion, but I am a little nervous that this may
turn out to be one of those cases where the obvious API ends up
working less well than might have been supposed.

What are your thoughts about a "not-obvious api"?

It seems to me (and it may seem differently to other people) that what
most people who want to trap DDL events really want to do is either
(a) get control at the permissions-checking stage so that they can
override the system's default access control policy, either to allow
something that would normally be denied (as in Dimitri's sudo example)
or deny something that would normally be allowed (as with sepgsql, or
the example in the documentation for Dimitri's patch showing how to
enforce relation naming conventions) or else (b) perform some
additional steps after completing the operation (e.g. audit it,
replicate it, apply a security label to it). Much of the
selinux-related work that KaiGai Kohei and I have been doing over the
last couple of years has revolved around where to put those hooks and
what information to pass to them.

For example, we added some post-creation hooks where the system
basically says "we just created a <object-type> and its OID is
<newly-assigned-oid>". I'm not going to present this as a model of
usability (because it isn't). However, I think the underlying model
is worth considering. It's not a command trigger, because it doesn't
care what command the user executed that led to the object getting
created. It's more like an event system - whenever a table gets
created (no matter how exactly that happens), you get a callback. I
think that's probably closer to what people want for the
additional-steps-after-completing-the-operation use case. Typically,
you're not really interested in what the user typed: you want to know
what the system decided to do about it.

The "get control at the permissions checking stage" use case is a bit
harder. We've added some hooks for that as well, but really only for
DML thus far, and I'm not convinced we've got the right design even
there. Part of the complexity here is that there are actually a lot
of different permissions rules depending on exactly what operation you
are performing - you might think of REINDEX TABLE, ALTER TABLE ..
ALTER COLUMN .. RENAME and DROP TABLE as requiring the same
permissions (i.e. ownership) but in fact they're all slightly
different. It would be nice if the permissions checking machinery
were better separated from the code that actually does the work, so
that you could rip and replace just that part. Dimitri's idea of
using a security definer function as a DDL instead-of trigger is an
interesting one, but does that leave newly created objects owned by
the wrong user? Or what if you want user A to be able to do some but
not all of the things user B can do? Drilling an optimally-sized hole
through the existing permissions system is unfortunately not very
simple.

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

#33Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#32)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

It seems to me (and it may seem differently to other people) that what
most people who want to trap DDL events really want to do is either

[ detailed analysis, mostly right on spot ]

Yeah, I'm proposing a rather crude tool. I think it's still solving
real problems we have now, and that no other tool nor design is solving
for us.

So for me the real questions are:

- do we want the feature in that form?
- are we avoiding to paint ourselves into a corner?

I think both answers are positive, because I want the feature bad enough
to have spent time working on it, and it's able to solve two problems in
one stone already. Also, the only way to have that feature in an
extension implementing ProcessUtility_hook is duplicating what I've been
doing, minus grammar support (just because you can't).

Also that's not the kind of efforts that either slony or londiste will
put into their own project, the amount of work wouldn't be justified for
this only return, as history is telling us. (there's a game changer
here though, which is the idea of doing “command triggers” as opposed to
“ddl triggers” or even “catalog triggers”, thanks Jan)

Then, we can expand the trigger function signature down the road and
keep the current one as a compatibility support. For example we could
add a cascading boolean argument and decide whether or not to call
the trigger function when cascading based upon the trigger's procedure
signature.

So I believe it's somewhat coarse or crude, still useful enough, and not
painting us into a corner.

using a security definer function as a DDL instead-of trigger is an
interesting one, but does that leave newly created objects owned by
the wrong user? Or what if you want user A to be able to do some but
not all of the things user B can do? Drilling an optimally-sized hole
through the existing permissions system is unfortunately not very
simple.

You can still use ALTER OBJECT … OWNER TO …; from within the security
definer function to allow the setup you're interested into. I guess
that you can still have some conditional on current_user from the code
too, but I didn't check that we have user id and effective user id both
available from the SQL level (yet).

All in all, I understand your reticence here, but I'm not sharing it,

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#34Greg Smith
greg@2ndQuadrant.com
In reply to: Robert Haas (#28)
Re: Command Triggers

On 12/12/2011 11:32 AM, Robert Haas wrote:

I haven't yet thought about your specific proposal here in enough to
have a fully-formed opinion, but I am a little nervous that this may
turn out to be one of those cases where the obvious API ends up
working less well than might have been supposed.

There's another cautionary tale from the sepgsql history worth
mentioning here, which surely I don't have to remind you about. Making
the goal for a first shippable subset include proof you can solve the
hardest problems in that area can lead to a long road without committing
anything. With sepgsql, that was focusing on the worst of the ALTER
TABLE issues. As Dimitri was pointing out, the name change to Command
Triggers includes a sort of admission that DDL Triggers are too hard to
solve in all cases yet. We shouldn't be as afraid to introduce APIs
that are aimed at developers who currently have none.

Yes, there's a risk that will end with "...and this one has to be broken
in the next release because of this case we didn't see". We can't be so
afraid of that we don't do anything, especially when the users who would
be impacted by that theoretical case are currently suffering from an
even worse problem than that. To provide the big picture infrastructure
tools that people are desperate for now, PostgreSQL needs to get a lot
more agile when it comes to revving hooks whose main consumers are not
regular application programs. They're the administrators of the system
instead.

I know what I was just rallying against is not what you were arguing
for, you just triggered a stored rant of mine. [Bad trigger joke goes
here] Regardless, thoughts on where the holes are here are appreciated.

--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.us

#35Robert Haas
robertmhaas@gmail.com
In reply to: Greg Smith (#34)
Re: Command Triggers

On Tue, Dec 13, 2011 at 8:25 AM, Greg Smith <greg@2ndquadrant.com> wrote:

There's another cautionary tale from the sepgsql history worth mentioning
here, which surely I don't have to remind you about.  Making the goal for a
first shippable subset include proof you can solve the hardest problems in
that area can lead to a long road without committing anything.  With
sepgsql, that was focusing on the worst of the ALTER TABLE issues.  As
Dimitri was pointing out, the name change to Command Triggers includes a
sort of admission that DDL Triggers are too hard to solve in all cases yet.
 We shouldn't be as afraid to introduce APIs that are aimed at developers
who currently have none.

Yes, there's a risk that will end with "...and this one has to be broken in
the next release because of this case we didn't see".

Well, the problem is that just because something better comes along
doesn't mean we'll actually deprecate and remove the old
functionality. We still have contrib/xml2, even though the docs say
we're "planning" to remove it in 8.4. Tom even rewrote the memory
handling, because it was easier to rewrite a module he probably
doesn't intrinsically care much about than to convince people we
should remove something that was both planned for deprecation anyway
and a huge security risk because it crashed if you looked at it
sideways. And we still have rules, so people read the documentation
and say to themselves "hmm, should i use triggers or rules for this
project?". And elsewhere we're discussing whether and under what
conditions it would be suitable to get rid of recovery.conf, which
almost everyone seems to agree is a poor design, largely AFAICT
because third-party tools find recovery.conf a convenient way to
circumvent the need to rewrite postgresql.conf, which is a pain in the
neck because of our insistence that it has to contain arbitrary user
comments. In other words, more often than not, we are extremely
reluctant to remove or modify features of even marginal utility
because there will certainly be somebody, somewhere who is relying on
the old behavior.

Of course, it does go the other way sometimes: we removed old-style
VACUUM FULL (which was useful if you were short of disk space and long
on time), flat-file authentication (which was used by third party
tools), and made removing OIDs require a table rewrite. Still, I
think it's entirely appropriate to be cautious about adding new
features that might not turn out to be the design we really want to
have. Odds are good that we will end up supporting them for backward
compatibility reasons for many, many years.

Now, all that having been said, I also agree that the perfect can be
the enemy of the good, and we go there frequently. The question I'm
asking is not whether the feature is perfect, but whether it's
adequate for even the most basic things people might want to do with
it. Dimitri says that he wants it so that we can add support for
CREATE TABLE, ALTER TABLE, and DROP TABLE to Slony, Bucardo, and
Londiste. My fear is that it won't turn out to be adequate to that
task, because there won't actually be enough information in the CREATE
TABLE statement to do the same thing on all servers. In particular,
you won't have the index or constraint names, and you might not have
the schema or tablespace information either. But maybe we ought to
put the question to the intended audience for the feature - is there a
Slony developer in the house?

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

#36Merlin Moncure
mmoncure@gmail.com
In reply to: Robert Haas (#35)
Re: Command Triggers

On Tue, Dec 13, 2011 at 8:59 AM, Robert Haas <robertmhaas@gmail.com> wrote:

Now, all that having been said, I also agree that the perfect can be
the enemy of the good, and we go there frequently.  The question I'm
asking is not whether the feature is perfect, but whether it's
adequate for even the most basic things people might want to do with
it.  Dimitri says that he wants it so that we can add support for
CREATE TABLE, ALTER TABLE, and DROP TABLE to Slony, Bucardo, and
Londiste.  My fear is that it won't turn out to be adequate to that
task, because there won't actually be enough information in the CREATE
TABLE statement to do the same thing on all servers.  In particular,
you won't have the index or constraint names, and you might not have
the schema or tablespace information either.

But, you could query all that out from the system catalogs right?
Maybe a better facility should exist to convert a table name to a
create table statement than hand rolling it or invoking pg_dump, but
that's a separate issue.

This feature fills an important niche given that you can't hook RI
triggers to system catalogs...it comes up (in short, +1).

merlin

#37Robert Haas
robertmhaas@gmail.com
In reply to: Merlin Moncure (#36)
Re: Command Triggers

On Tue, Dec 13, 2011 at 10:53 AM, Merlin Moncure <mmoncure@gmail.com> wrote:

On Tue, Dec 13, 2011 at 8:59 AM, Robert Haas <robertmhaas@gmail.com> wrote:

Now, all that having been said, I also agree that the perfect can be
the enemy of the good, and we go there frequently.  The question I'm
asking is not whether the feature is perfect, but whether it's
adequate for even the most basic things people might want to do with
it.  Dimitri says that he wants it so that we can add support for
CREATE TABLE, ALTER TABLE, and DROP TABLE to Slony, Bucardo, and
Londiste.  My fear is that it won't turn out to be adequate to that
task, because there won't actually be enough information in the CREATE
TABLE statement to do the same thing on all servers.  In particular,
you won't have the index or constraint names, and you might not have
the schema or tablespace information either.

But, you could query all that out from the system catalogs right?

You could probably get a lot of it that way, although first you'll
have to figure out which schema to look up the name in. It seems
likely that everyone who uses the trigger will need to write that
code, though, and they'll all have different implementations with
different bugs, because many of them probably really want the facility
that you write in your next sentence:

Maybe a better facility should exist to convert a table name to a
create table statement than hand rolling it or invoking pg_dump, but
that's a separate issue.

This feature fills an important niche given that you can't hook RI
triggers to system catalogs...it comes up (in short, +1).

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

#38Christopher Browne
cbbrowne@gmail.com
In reply to: Robert Haas (#35)
Re: Command Triggers

On Tue, Dec 13, 2011 at 9:59 AM, Robert Haas <robertmhaas@gmail.com> wrote:

Now, all that having been said, I also agree that the perfect can be
the enemy of the good, and we go there frequently.  The question I'm
asking is not whether the feature is perfect, but whether it's
adequate for even the most basic things people might want to do with
it.  Dimitri says that he wants it so that we can add support for
CREATE TABLE, ALTER TABLE, and DROP TABLE to Slony, Bucardo, and
Londiste.  My fear is that it won't turn out to be adequate to that
task, because there won't actually be enough information in the CREATE
TABLE statement to do the same thing on all servers.  In particular,
you won't have the index or constraint names, and you might not have
the schema or tablespace information either.  But maybe we ought to
put the question to the intended audience for the feature - is there a
Slony developer in the house?

Yeah, I'm not certain yet what is being provided, and the
correspondence with what would be needed.

- It's probably not sufficient to capture the raw statement, as that
gets invoked within a context replete with GUC values, and you may
need to duplicate that context/environment on a replica. Mind you, a
command trigger is doubtless capable of querying GUCs to duplicate
relevant portions of the environment.

- What I'd much rather have is a form of the query that is replete
with Full Qualification, so that "create table foo (id serial primary
key, data text not null unique default 'better replace this', dns_data
dnsrr not null);" may be transformed into a safer form like: "create
table public.foo (id serial primary key, data text not null unique
default 'better replace this'::text, dns_data dns_rr.dnsrr not null);"

What's not clear, yet, is which transformations are troublesome. For
instance, if there's already a sequence called "foo_id_seq", then the
sequence defined for that table winds up being "foo_id_seq1", and it's
not quite guaranteed that *that* would be identical across databases.

But perhaps it's sufficient to implement what, of COMMAND TRIGGERS,
can be done, and we'll see, as we proceed, whether or not it's enough.

It's conceivable that a first implementation won't be enough to
implement DDL triggers for Slony, and that we'd need to ask for
additional functionality that doesn't make it in until 9.3. That
seems better, to me, than putting it on the shelf, and having
functionality in neither 9.2 nor 9.3...
--
When confronted by a difficult problem, solve it by reducing it to the
question, "How would the Lone Ranger handle this?"

#39Robert Haas
robertmhaas@gmail.com
In reply to: Christopher Browne (#38)
Re: Command Triggers

On Tue, Dec 13, 2011 at 12:29 PM, Christopher Browne <cbbrowne@gmail.com> wrote:

But perhaps it's sufficient to implement what, of COMMAND TRIGGERS,
can be done, and we'll see, as we proceed, whether or not it's enough.

It's conceivable that a first implementation won't be enough to
implement DDL triggers for Slony, and that we'd need to ask for
additional functionality that doesn't make it in until 9.3.  That
seems better, to me, than putting it on the shelf, and having
functionality in neither 9.2 nor 9.3...

The thing is, I don't really see the approach Dimitri is taking as
being something that we can extend to meet the requirement you just
laid out. So it's not like, OK, let's do this, and we'll improve it
later. It's, let's do this, and then later do something completely
different, and that other thing will be the one that really solves the
problem.

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

#40Greg Smith
greg@2ndQuadrant.com
In reply to: Robert Haas (#35)
Re: Command Triggers

On 12/13/2011 09:59 AM, Robert Haas wrote:

Well, the problem is that just because something better comes along
doesn't mean we'll actually deprecate and remove the old
functionality.

The examples you gave fall into three groups, and I think it's useful to
demarcate how they're different. Please pardon me going wildly off
topic before returning back.

If you drop xml2 or rules, people lose something. It's primarily the
PostgreSQL developers who gain something. You can make a case that
people who won't get sucked into doing something wrong with rules one
day will gain something, but those people are a future speculation;
they're not here asking to be saved for a problem they don't know will
happen yet. This sort of deprecation battle is nearly impossible to
win. One of the reasons I placed a small bet helping sponsor PGXN is
that I hope it allows some of this should be deprecated stuff to move
there usefully. Let the people who use it maintain it moving forward if
they feel it's important.

The recovery.conf change and other attempts to reorganize the
postgresql.conf are in a second category. These break scripts, without
providing an immediate and obvious gain to everyone. Some say it's
better, some say it's worse, from the list traffic it seems like a
zero-sum game. The burden is on the person advocating the change to
justify it if there's not a clear win for everyone. You might note that
my latest attitude toward this area is to provide the mechanism I want
as a new option, and not even try to argue about removing the old thing
anymore. This lets implementation ideas battle it out in the wild.
Let's say a year from now everyone who hasn't switched to using a conf.d
dirctory approach looks like an old stick in the mud. Then maybe I pull
the sheet I have an enormous bikeshed hidden behind, waiting for just
that day.[1]Look at that, I can now say that 100% of the programs I compose e-mail with now have "bikeshed" added to their dictionary. I don't bother with this often, but there's entries for "PostgreSQL" and "committer" there too.[2]

When VACUUM FULL was rewritten, it took a recurring large problem that
has impacted a lot of people, and replaced with a much better thing for
most cases. A significantly smaller number of people lost something
that was slightly useful. There weren't as many complaints because the
thing that was lost was replaced with something better by most metrics.
Different, but better. This third category of changes are much easier
to sell. We have another such discontinuity coming with
pg_stat_activity. The changes Scott Mead's patch kicked off make it
different and better. Anyone who has a tool using the old thing can
look at the new design and say "well, that makes the whole 'what state
is the connection in' logic I used to worry about go away; that's
progress even if it breaks my old scripts". People need to get
something that offsets the breakage to keep griping down. Anyone who
argues against those sort of changes has a challenging battle on their
hands.

If there is a Command Trigger implementation that Slony etc. use, and we
discover a limitation that requires an API break, that's OK so long as
it's expected that will fall into the last category. Breakage to add
support for something new should be a feature clearly gained, something
lost, and with a net benefit to most consumers of that feature. People
accept it or block obvious forward progress. We don't want to get too
confused between what makes for good progress on that sort of thing with
the hard deprecation problems. (Not that I'm saying you are here, just
pointing out it happens)

Dimitri says that he wants it so that we can add support for
CREATE TABLE, ALTER TABLE, and DROP TABLE to Slony, Bucardo, and
Londiste. My fear is that it won't turn out to be adequate to that
task, because there won't actually be enough information in the CREATE
TABLE statement to do the same thing on all servers.

These are all good things to look into, please keep those test set ideas
coming and hopefully we'll get some more input on this. But let's say
this rolls out seeming good enough, and later someone discovers some
weird index thing that isn't supported. If that's followed by "here's a
new API; it breaks your old code, but it allows supporting that index
you couldn't deal with before", that's unlikely to get shot down by that
API's consumers. What you wouldn't be able to do is say "this new API
doesn't work right, let's just yank it out". Your concerns about making
sure at least the fundamentals hold here are on point though.

[1]: Look at that, I can now say that 100% of the programs I compose e-mail with now have "bikeshed" added to their dictionary. I don't bother with this often, but there's entries for "PostgreSQL" and "committer" there too.[2]
e-mail with now have "bikeshed" added to their dictionary. I don't
bother with this often, but there's entries for "PostgreSQL" and
"committer" there too.[2]Would you believe a Google search for "committer" shows the PostgreSQL wiki page as its second hit? That's only behind the Wikipedia link, and ahead of the FreeBSD, Chromium, Apache, and Mozilla pages on that topic.

[2]: Would you believe a Google search for "committer" shows the PostgreSQL wiki page as its second hit? That's only behind the Wikipedia link, and ahead of the FreeBSD, Chromium, Apache, and Mozilla pages on that topic.
PostgreSQL wiki page as its second hit? That's only behind the
Wikipedia link, and ahead of the FreeBSD, Chromium, Apache, and Mozilla
pages on that topic.

--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.us

#41Jan Wieck
JanWieck@Yahoo.com
In reply to: Robert Haas (#35)
Re: Command Triggers

On 12/13/2011 9:59 AM, Robert Haas wrote:

it. Dimitri says that he wants it so that we can add support for
CREATE TABLE, ALTER TABLE, and DROP TABLE to Slony, Bucardo, and
Londiste. My fear is that it won't turn out to be adequate to that
task, because there won't actually be enough information in the CREATE
TABLE statement to do the same thing on all servers. In particular,
you won't have the index or constraint names, and you might not have
the schema or tablespace information either. But maybe we ought to
put the question to the intended audience for the feature - is there a
Slony developer in the house?

I agree. While it is one of the most "asked for" features among the
trigger based replication systems, I fear that an incomplete solution
will cause more problems than it solves. It is far easier to tell people
"DDL doesn't propagate automatically, do this instead ..." than to try
to support a limited list of commands, that may or may not propagate as
intended. And all sorts of side effects, like search_path, user names
and even the existing schema in the replica can cause any given DDL
"string" to actually do something completely different than what
happened on the origin.

On top of that, the PostgreSQL main project has a built in replication
solution that doesn't need any of this. There is no need for anyone, but
us trigger replication folks, to keep command triggers in sync with all
other features.

I don't think it is going to be reliable enough any time soon to make
this the default for any of the trigger based replication systems.

Jan

--
Anyone who trades liberty for security deserves neither
liberty nor security. -- Benjamin Franklin

#42Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#35)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

it. Dimitri says that he wants it so that we can add support for
CREATE TABLE, ALTER TABLE, and DROP TABLE to Slony, Bucardo, and
Londiste. My fear is that it won't turn out to be adequate to that
task, because there won't actually be enough information in the CREATE
TABLE statement to do the same thing on all servers. In particular,
you won't have the index or constraint names, and you might not have
the schema or tablespace information either.

In my experience of managing lots of trigger based replications (more
than 100 nodes in half a dozen different projects), what I can tell from
the field is that I don't care about index and constraint names. Being
able to replicate the same CREATE TABLE statement that the provider just
executed on the subscriber is perfectly fine for my use cases.

Again, that's a caveat of the first implementation, you can't have sub
commands support without forcing them through ProcessUtility and that's
a much more invasive patch. Maybe we will need that later.

Also it's quite easy to add support for the CREATE INDEX command,
including index name support, and ALTER TABLE is already on the go. So
we can document how to organize your DDL scripts for them to just work
with the replication system. And you can even implement a command
trigger that enforces respecting the limits (RAISE EXCEPTION when the
CREATE TABLE command is embedding primary key creation rather than using
a separate command for that).

As for the schema, you can easily get the current search_path setting
from the command trigger and force it to the same value on the
subscriber before replaying the commands (hint: add current search_path
to the event you're queuing for replay).

select setting from pg_settings where name = 'search_path';

I appreciate that some use cases won't be possible to implement with the
first version of this patch, really, but I believe we have enough use
cases that are possible to implement with it that it's worth providing
the feature.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#43Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Christopher Browne (#38)
Re: Command Triggers

Christopher Browne <cbbrowne@gmail.com> writes:

- What I'd much rather have is a form of the query that is replete
with Full Qualification, so that "create table foo (id serial primary
key, data text not null unique default 'better replace this', dns_data
dnsrr not null);" may be transformed into a safer form like: "create
table public.foo (id serial primary key, data text not null unique
default 'better replace this'::text, dns_data dns_rr.dnsrr not null);"

Andres did the same comment and I've begun working on that. The
facility for fully qualifying object names are not always available in a
form that we can use from the parsetree, but that's a SMOP.

What's not clear, yet, is which transformations are troublesome. For
instance, if there's already a sequence called "foo_id_seq", then the
sequence defined for that table winds up being "foo_id_seq1", and it's
not quite guaranteed that *that* would be identical across databases.

But perhaps it's sufficient to implement what, of COMMAND TRIGGERS,
can be done, and we'll see, as we proceed, whether or not it's enough.

I'm not convinced that having the same sequence names on the subscribers
is something we need here. Let me detail that, because maybe I just
understood a major misunderstanding in the use case I'm interested into.

I mostly use trigger based replication in cases where it's not possible
to implement failover or switchover with that technique, because I'm
doing cross-replication or more complex architectures. Failover is
handled with WAL based techniques (wal shipping, streaming rep, etc).

So I don't care that much about the sub object names (constraints,
indexes, sequences): I need them to get created on the subscriber and
that's about it. I think that's an important enough use case here.

It's conceivable that a first implementation won't be enough to
implement DDL triggers for Slony, and that we'd need to ask for
additional functionality that doesn't make it in until 9.3. That
seems better, to me, than putting it on the shelf, and having
functionality in neither 9.2 nor 9.3...

Or maybe Slony would end up relying partly on the command trigger
facility and implementing the missing pieces in its own code base. Like
it did with the txid_snapshot data type some years ago, for example.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#44Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Jan Wieck (#41)
Re: Command Triggers

Jan Wieck <JanWieck@Yahoo.com> writes:

I agree. While it is one of the most "asked for" features among the trigger
based replication systems, I fear that an incomplete solution will cause
more problems than it solves. It is far easier to tell people "DDL doesn't
propagate automatically, do this instead ..." than to try to support a
limited list of commands, that may or may not propagate as intended. And all

Nothing stops you from checking that the command you want to replicate
is indeed supported and refuse to run it on the provider when not,
that's what command triggers are for :)

sorts of side effects, like search_path, user names and even the existing
schema in the replica can cause any given DDL "string" to actually do
something completely different than what happened on the origin.

Grab those on the provider from pg_settings and the like in the command
trigger and restore them on the subscriber before applying the command?

On top of that, the PostgreSQL main project has a built in replication
solution that doesn't need any of this. There is no need for anyone, but us
trigger replication folks, to keep command triggers in sync with all other
features.

You can't implement cross replication with built-in replication. I'm yet
to work on a medium sized project where I don't need both streaming
replication, wal archiving, and a trigger based replication system.

I don't think it is going to be reliable enough any time soon to make this
the default for any of the trigger based replication systems.

You need to add "yet" and "without some work in the client implementation".

Last time I read such comments, it was about extensions. We still
shipped something in 9.1, and by your measure, it's quite broken. When
you implement an SQL only extension (no .so) you still have to use a
Makefile and you need to deploy your scripts on the server filesystem
before hand, it's not possible to install or update the extension from
an SQL connection (even when doing only self-contained SQL commands).

Also, the dependency system is not solving much real world problems,
apart from the very simplest one that we can play with in contrib/.

You can't even list shared objects (.so, .dll) that have been loaded so
far in a session and reference the extension they belong to.

Yet, I bet that some people will find that the extension system as we
have it in 9.1 still is useful enough to have been released in there.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

PS: yes I intend to be working on fixing those extension limitations and
caveats with a series of patches.

#45Alvaro Herrera
alvherre@commandprompt.com
In reply to: Dimitri Fontaine (#42)
Re: Command Triggers

Excerpts from Dimitri Fontaine's message of mié dic 14 07:22:21 -0300 2011:

Again, that's a caveat of the first implementation, you can't have sub
commands support without forcing them through ProcessUtility and that's
a much more invasive patch. Maybe we will need that later.

I can get behind this argument: force all stuff through ProcessUtility
for regularity, and not necessarily in the first patch for this feature.

--
Álvaro Herrera <alvherre@commandprompt.com>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support

#46Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#45)
Re: Command Triggers

On Wed, Dec 14, 2011 at 9:05 AM, Alvaro Herrera
<alvherre@commandprompt.com> wrote:

Excerpts from Dimitri Fontaine's message of mié dic 14 07:22:21 -0300 2011:

Again, that's a caveat of the first implementation, you can't have sub
commands support without forcing them through ProcessUtility and that's
a much more invasive patch.  Maybe we will need that later.

I can get behind this argument: force all stuff through ProcessUtility
for regularity, and not necessarily in the first patch for this feature.

That seems like a pretty heavy dependency on an implementation detail
that we might want to change at some point.

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

#47Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#46)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

I can get behind this argument: force all stuff through ProcessUtility
for regularity, and not necessarily in the first patch for this feature.

That seems like a pretty heavy dependency on an implementation detail
that we might want to change at some point.

Given ProcessUtility_hook, how much of an implementation detail rather
than a public API are we talking about?

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#48Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#47)
Re: Command Triggers

On Wed, Dec 14, 2011 at 5:44 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

I can get behind this argument: force all stuff through ProcessUtility
for regularity, and not necessarily in the first patch for this feature.

That seems like a pretty heavy dependency on an implementation detail
that we might want to change at some point.

Given ProcessUtility_hook, how much of an implementation detail rather
than a public API are we talking about?

I think that a hook and an SQL command are not on the same level.
Hooks are not documented; SQL commands are. You may, of course,
disagree.

But the basic point is that it seems pretty weird to say, on the one
hand, these are command triggers. They fire when you execute a
command. So the CREATE TABLE trigger will fire when someone says
CREATE TABLE. But then we say, oh, well if you use CREATE SCHEMA foo
CREATE TABLE blah ... we will fire the trigger anyway. Now it's not a
command trigger any more; it's a trigger that fires when you perform a
certain operation - e.g. create a table. Unless, of course, you
create the table using CREATE TABLE AS SELECT or SELECT .. INTO. Then
it doesn't fire. Unless we decide to make those utility commands,
which I think was just recently under discussion, in which case it
will suddenly start firing for those operations. So now something
that is, right now, an essentially an implementation detail which we
can rearrange as we like turns into a user-visible behavior change.
And what if some day we want to change it back?

I think it would be a much better idea to decree from the beginning
that we're trapping the *operation* of creating a table, rather than
the *command* create table. Then, the behavior is clear and
well-defined from day one, and it doesn't depend on how we happen to
implement things internally right at the moment. If there's some way
of creating a table without firing the trigger, it's a bug. If
there's some way of firing the trigger without attempting to create a
table, it's a bug. That might require some more thought about what
information to pass to the trigger function (e.g. if it's a SELECT ..
INTO, you're not going to have pregenerated SQL that starts with the
words "CREATE TABLE") but the fact that gives much more clear
definition to the core feature seems like a big plus in my book.

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

#49Andres Freund
andres@anarazel.de
In reply to: Robert Haas (#48)
Re: Command Triggers

On Thursday, December 15, 2011 03:36:25 PM Robert Haas wrote:

On Wed, Dec 14, 2011 at 5:44 PM, Dimitri Fontaine

<dimitri@2ndquadrant.fr> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

I can get behind this argument: force all stuff through ProcessUtility
for regularity, and not necessarily in the first patch for this
feature.

That seems like a pretty heavy dependency on an implementation detail
that we might want to change at some point.

Given ProcessUtility_hook, how much of an implementation detail rather
than a public API are we talking about?

I think that a hook and an SQL command are not on the same level.
Hooks are not documented; SQL commands are. You may, of course,
disagree.

I am not disagreeing.

But the basic point is that it seems pretty weird to say, on the one
hand, these are command triggers. They fire when you execute a
command. So the CREATE TABLE trigger will fire when someone says
CREATE TABLE. But then we say, oh, well if you use CREATE SCHEMA foo
CREATE TABLE blah ... we will fire the trigger anyway. Now it's not a
command trigger any more; it's a trigger that fires when you perform a
certain operation - e.g. create a table. Unless, of course, you
create the table using CREATE TABLE AS SELECT or SELECT .. INTO. Then
it doesn't fire. Unless we decide to make those utility commands,
which I think was just recently under discussion, in which case it
will suddenly start firing for those operations.

Command triggers were the initial reason for me doing that conversion.

So now something
that is, right now, an essentially an implementation detail which we
can rearrange as we like turns into a user-visible behavior change.
And what if some day we want to change it back?

I think it would be a much better idea to decree from the beginning
that we're trapping the *operation* of creating a table, rather than
the *command* create table. Then, the behavior is clear and
well-defined from day one, and it doesn't depend on how we happen to
implement things internally right at the moment.

I am with you there.

If there's some way
of creating a table without firing the trigger, it's a bug. If
there's some way of firing the trigger without attempting to create a
table, it's a bug.

Again.

That might require some more thought about what
information to pass to the trigger function (e.g. if it's a SELECT ..
INTO, you're not going to have pregenerated SQL that starts with the
words "CREATE TABLE") but the fact that gives much more clear
definition to the core feature seems like a big plus in my book.

Not sure what youre getting at here? The command tag in Dim's patch is alread
independent of the form of actual statement that was run.

A big +1 on the general direction of this email. I dislike reducing the scope
of command triggers to "top level commands run by the actual user" because imo
that would reduce the possible scope of the patch rather much.

On the other hand I think delivering a complete patch covering just about
anything triggered anywhere is not really realistic. Not sure whats the best
way to continue is.

Andres

#50Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#48)
Re: Command Triggers

Hi,

Thank your Robert for this continued review, I think we're making good
progress in a community discussion that needs to happen!

Robert Haas <robertmhaas@gmail.com> writes:

Given ProcessUtility_hook, how much of an implementation detail rather
than a public API are we talking about?

I think that a hook and an SQL command are not on the same level.
Hooks are not documented; SQL commands are. You may, of course,
disagree.

Mmmm, yes. I think that we should document hooks, and I'm going to
propose soon a pg_extension_module() SRF that lists all currently loaded
modules and which extension implements them, and I've begun thinking
about what it would take to be able to list what hooks each module is
implementing.

That would require an hook registration API, and would allow a much
easier security auditing of a setup you don't know beforehand.

So I think that hooks not being documented is an implementation detail
here :)

But the basic point is that it seems pretty weird to say, on the one
hand, these are command triggers. They fire when you execute a
command. So the CREATE TABLE trigger will fire when someone says
CREATE TABLE. But then we say, oh, well if you use CREATE SCHEMA foo
CREATE TABLE blah ... we will fire the trigger anyway. Now it's not a

Yes, this CREATE SCHEMA <any create command> is weird, and unique, so
it's not that difficult to document, I think. And if we fix things by
having all subcommands go through ProcessUtility and command triggers,
then it's easy to document as the new rule.

My documentation idea would then be explaining what is a command and
what is a subcommand, and then the current rule (command triggers do not
support subcommands) and then the exception to that rule (CREATE SCHEMA
subcommands do, in fact, support command triggers).

If we decide that we should in fact support (nested) subcommands in
command triggers, then we will be in a position to prepare a patch
implementing that with a documentation change that the rule is now that
command triggers do in fact support subcommands.

When the command trigger is called on a subcommand, I think you will
want both the main command string and the subcommand string, and we have
to research if what you need isn't a whole stack of commands, because
I'm not sure you can only have 1 level deep nesting here.

That would be done with a special API for the trigger functions, and
with a specific syntax, because it seems to me that having a trigger
code that is only ever called on a top-level command is a good thing to
have.

create trigger foo after command CREATE TABLE …
create trigger foo after top level command CREATE TABLE …
create trigger foo after nested command CREATE TABLE …

Either we add support for that kind of syntax now (and not implementing
it in 9.3 would seem weird as hell) or we instruct pg_dump and
pg_upgrade to switch from current syntax to the new one (add in the “top
level” keywords) when we do implement the feature down the road.

command trigger any more; it's a trigger that fires when you perform a
certain operation - e.g. create a table. Unless, of course, you
create the table using CREATE TABLE AS SELECT or SELECT .. INTO. Then
it doesn't fire. Unless we decide to make those utility commands,
which I think was just recently under discussion, in which case it
will suddenly start firing for those operations. So now something

Andres has been sending a complete patch to fix that old oddity of the
parser. And again, that's a very narrow exception to the usual state of
things, and as such, easy to document in the list of things that don't
fall into the general rule.

Even when fixed, though, you have 3 different command tags to deal with,
and we identify the command triggers on command tags, so that a command
trigger on CREATE TABLE will not be called when doing SELECT INTO, nor
when doing CREATE TABLE AS.

Also, I think that the command triggers in 9.2 will not address all and
any command that PostgreSQL offers (think “alter operator family”), so
that we will need to maintain an exhaustive list of supported commands,
identified by command tags.

that is, right now, an essentially an implementation detail which we
can rearrange as we like turns into a user-visible behavior change.
And what if some day we want to change it back?

Yes, we need to make a decision about that now. Do we want any
“operation” to go through ProcessUtility so that hooks and command
triggers can get called?

I think it's a good idea in the long run, and Alvaro seems to be
thinking it is too. That entails changing the backend code to build a
Statement then call ProcessUtility on it rather than calling the
internal functions directly, and that also means some more DDL are
subject to being logged on the server logs. Removing those
“cross-modules” #include might be a good think in the long run though.

And we don't have to do that unless we want to make subcommands
available to ProcessUtility_hook and command triggers.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#51Andres Freund
andres@anarazel.de
In reply to: Dimitri Fontaine (#50)
Re: Command Triggers

On Thursday, December 15, 2011 04:53:15 PM Dimitri Fontaine wrote:

Hi,

Thank your Robert for this continued review, I think we're making good
progress in a community discussion that needs to happen!

Robert Haas <robertmhaas@gmail.com> writes:

Given ProcessUtility_hook, how much of an implementation detail rather
than a public API are we talking about?

I think that a hook and an SQL command are not on the same level.
Hooks are not documented; SQL commands are. You may, of course,
disagree.

Mmmm, yes. I think that we should document hooks, and I'm going to
propose soon a pg_extension_module() SRF that lists all currently loaded
modules and which extension implements them, and I've begun thinking
about what it would take to be able to list what hooks each module is
implementing.

I don't really see that as possible/realistic.

But the basic point is that it seems pretty weird to say, on the one
hand, these are command triggers. They fire when you execute a
command. So the CREATE TABLE trigger will fire when someone says
CREATE TABLE. But then we say, oh, well if you use CREATE SCHEMA foo
CREATE TABLE blah ... we will fire the trigger anyway. Now it's not a

Yes, this CREATE SCHEMA <any create command> is weird, and unique, so
it's not that difficult to document, I think. And if we fix things by
having all subcommands go through ProcessUtility and command triggers,
then it's easy to document as the new rule.

I don't think I ever saw that one in real worldddl scripts ;)

My documentation idea would then be explaining what is a command and
what is a subcommand, and then the current rule (command triggers do not
support subcommands) and then the exception to that rule (CREATE SCHEMA
subcommands do, in fact, support command triggers).

If we decide that we should in fact support (nested) subcommands in
command triggers, then we will be in a position to prepare a patch
implementing that with a documentation change that the rule is now that
command triggers do in fact support subcommands.

When the command trigger is called on a subcommand, I think you will
want both the main command string and the subcommand string, and we have
to research if what you need isn't a whole stack of commands, because
I'm not sure you can only have 1 level deep nesting here.

I don't think you need that. If needed you will have to build the data
structure in $pl.

That would be done with a special API for the trigger functions, and
with a specific syntax, because it seems to me that having a trigger
code that is only ever called on a top-level command is a good thing to
have.

create trigger foo after command CREATE TABLE …
create trigger foo after top level command CREATE TABLE …
create trigger foo after nested command CREATE TABLE …

Either we add support for that kind of syntax now (and not implementing
it in 9.3 would seem weird as hell) or we instruct pg_dump and
pg_upgrade to switch from current syntax to the new one (add in the “top
level” keywords) when we do implement the feature down the road.

I personally think there should only be one variant which is always called.
With a parameter that indicates whether its a subcommand or not.

Why would you ever only want top level commands?

that is, right now, an essentially an implementation detail which we
can rearrange as we like turns into a user-visible behavior change.
And what if some day we want to change it back?

Yes, we need to make a decision about that now. Do we want any
“operation” to go through ProcessUtility so that hooks and command
triggers can get called?

I personally think thats a good idea for most stuff.

I don't see that for alter table subcommands and such though.

I think it's a good idea in the long run, and Alvaro seems to be
thinking it is too. That entails changing the backend code to build a
Statement then call ProcessUtility on it rather than calling the
internal functions directly, and that also means some more DDL are
subject to being logged on the server logs. Removing those
“cross-modules” #include might be a good think in the long run though.

Uhm. I don't think building strings is the way to go here. I think building
*Stmt nodes is better.

Andres

#52Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Andres Freund (#51)
Re: Command Triggers

Andres Freund <andres@anarazel.de> writes:

create trigger foo after command CREATE TABLE …
create trigger foo after top level command CREATE TABLE …
create trigger foo after nested command CREATE TABLE …

Either we add support for that kind of syntax now (and not implementing
it in 9.3 would seem weird as hell) or we instruct pg_dump and
pg_upgrade to switch from current syntax to the new one (add in the “top
level” keywords) when we do implement the feature down the road.

I personally think there should only be one variant which is always called.
With a parameter that indicates whether its a subcommand or not.

Why would you ever only want top level commands?

Because the command "create table foo(id primary key)" could then fire
your "index" and "constraint" command triggers twice and if all you do
is storing the command string in a table for auditing purposes, maybe
you just don't care.

Yes, we need to make a decision about that now. Do we want any
“operation” to go through ProcessUtility so that hooks and command
triggers can get called?

I personally think thats a good idea for most stuff.

I don't see that for alter table subcommands and such though.

By subcommand, I mean any operation that a main command do for you and
that you could have been doing manually with another command, such as
serial and sequences, primary key and its alter table form, embedded
checks or not null and its alter table form, etc.

I don't remember that ALTER TABLE implement facilities that are in turn
calling another command for you?

I think it's a good idea in the long run, and Alvaro seems to be
thinking it is too. That entails changing the backend code to build a
Statement then call ProcessUtility on it rather than calling the
internal functions directly, and that also means some more DDL are
subject to being logged on the server logs. Removing those
“cross-modules” #include might be a good think in the long run though.

Uhm. I don't think building strings is the way to go here. I think building
*Stmt nodes is better.

Agreed, I meant that exactly.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#53Noah Misch
noah@leadboat.com
In reply to: Dimitri Fontaine (#52)
Re: Command Triggers

On Thu, Dec 15, 2011 at 05:46:05PM +0100, Dimitri Fontaine wrote:

Andres Freund <andres@anarazel.de> writes:

Yes, we need to make a decision about that now. Do we want any
???operation??? to go through ProcessUtility so that hooks and command
triggers can get called?

I personally think thats a good idea for most stuff.

I don't see that for alter table subcommands and such though.

By subcommand, I mean any operation that a main command do for you and
that you could have been doing manually with another command, such as
serial and sequences, primary key and its alter table form, embedded
checks or not null and its alter table form, etc.

I don't remember that ALTER TABLE implement facilities that are in turn
calling another command for you?

When ALTER TABLE ALTER TYPE changes an indexed column, it updates the index by
extracting its SQL definition, dropping it, and running that SQL. (Not sure
whether it passes through the code paths to hit your trigger.) We ought not to
promise that ALTER TABLE will always do so. By comparison, we could implement
ALTER TABLE SET NOT NULL on an inheritance tree as several ALTER TABLE ONLY, but
we don't implement it that way. Triggering on top-level commands can be quite
well-defined; triggers on subcommands (as you have defined the term) will risk
firing at different times across major versions. That may just be something to
document.

I think we'll need a way to tell SPI whether a command should be considered a
subcommand or not. A PL/pgSQL function that calls CREATE TABLE had better
fire a trigger, but some SPI usage will qualify as subcommands.

nm

#54Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#50)
Re: Command Triggers

On Thu, Dec 15, 2011 at 10:53 AM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Yes, we need to make a decision about that now. Do we want any
“operation” to go through ProcessUtility so that hooks and command
triggers can get called?

No. That's 100% backwards. We should first decide what functionality
we want, and then decide how we're going to implement it. If we
arbitrarily decide to force everything that someone might want to
write a trigger on through ProcessUtility_hook, then we're going to
end up being unable to add triggers for some things because they can't
be conveniently forced through ProcessUtility, or else we're going to
end up contorting the code in bizarre ways because we've drawn some
line in the sand that ProcessUtility is the place where triggers have
to get called.

In doing this project, I think we should pay a lot of attention to the
lessons that have been learned developing sepgsql. I can certainly
understand if your eyes roll back in your head when you hear that,
because that project has been exceedingly long and difficult and shows
no sign of reaching its full potential for at least another few
release cycles. But I think it's really worth the effort to avoid
pounding our heads against the brick wall twice. Two things that leap
out at me in this regard are:

(1) It's probably a mistake to assume that we only need one interface.
sepgsql has several hooks, and will need more before the dust
settles. We have one hook for checking permissions on table names
that appear in DML queries, a second for applying security labels just
after a new SQL object is created, and a third for adjusting the
security context when an sepgsql trusted procedure is invoked. In a
similar way, I think it's probably futile to think that we can come up
with a one-size-fits-all interface where every command (or operation)
trigger can accept the same arguments. CREATE TABLE is going to want
to know different stuff than LOCK TABLE or ALTER OPERATOR FAMILY, and
trigger writers are going to want a *convenient* API to that
information, not just the raw query text.

(2) It's almost certainly a mistake to assume that everything you want
to trigger on is a command. For example, somebody might want to get
control whenever a table gets added to a column, either at table
create time or later. I don't think most of us would consider CREATE
TABLE bob (a int, b int) to be a create-a-table-with-no-columns
operation plus two add-a-column-to-a-table operations. But being able
to enforce a column naming policy is no less useful than being able to
enforce a table naming policy, and there are other things you might
want to do there as well (logging, updating metadata, prohibiting use
of certain types, etc.).

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

#55Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#54)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

No. That's 100% backwards. We should first decide what functionality
we want, and then decide how we're going to implement it. If we
arbitrarily decide to force everything that someone might want to
write a trigger on through ProcessUtility_hook, then we're going to
end up being unable to add triggers for some things because they can't
be conveniently forced through ProcessUtility, or else we're going to
end up contorting the code in bizarre ways because we've drawn some
line in the sand that ProcessUtility is the place where triggers have
to get called.

In theory you're right. In practice, we're talking about utility
command triggers, that fires on a top-level command.

We're now enlarging the talk about what to do with sub-commands, that is
things done by a command as part of its implementation but that you
could have done separately with another finer grained dedicated
top-level command.

I'm not wanting to implement a general ”event” trigger mechanism where
anyone can come and help define the set of events, and I think what
you're talking about now amounts to be doing that.

Here again, trying to generalize before we have anything useful is a
recipe for failure. I concur that “Process Utility Top-Level Only
Command Triggers” is a pretty limited feature in scope, yet that's what
I want to obtain here, and I think it's useful enough on its own.

If you disagree, please propose a user level scheme where we can fit the
work I'm doing so that I can adapt my patch and implement a part of your
scheme in a future proof way. I'm ready to do that even when I have no
need for what you're talking about, if that's what it takes.

(1) It's probably a mistake to assume that we only need one interface.

[... useful sepgsql history ...]

trigger can accept the same arguments. CREATE TABLE is going to want
to know different stuff than LOCK TABLE or ALTER OPERATOR FAMILY, and
trigger writers are going to want a *convenient* API to that
information, not just the raw query text.

Are you familiar with the nodeToString() output? That's close to a full
blown XML, JSON or YAML document that you can easily use from a program.
Command triggers functions are not given just a raw text.

When trying to define a more complex API in the line of what you're
referencing here, back at the Cluster Hackers Developer Meeting, it was
devised that the nodeToString() output is all you need. For having
written code that produces it and code that consumes it, Jan has been
very clear that you hardly can do better than that while still being
generic enough.

to enforce a column naming policy is no less useful than being able to
enforce a table naming policy, and there are other things you might
want to do there as well (logging, updating metadata, prohibiting use
of certain types, etc.).

Are you familiar with the nodeToString() output? What makes you think
that the uses cases you propose here are hard to implement in a command
trigger when given this parse tree string?

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#56Bruce Momjian
bruce@momjian.us
In reply to: Dimitri Fontaine (#55)
Re: Command Triggers

Dimitri Fontaine wrote:

Here again, trying to generalize before we have anything useful is a
recipe for failure. I concur that ?Process Utility Top-Level Only
Command Triggers? is a pretty limited feature in scope, yet that's what
I want to obtain here, and I think it's useful enough on its own.

If you disagree, please propose a user level scheme where we can fit the
work I'm doing so that I can adapt my patch and implement a part of your
scheme in a future proof way. I'm ready to do that even when I have no
need for what you're talking about, if that's what it takes.

We have a big user community and what _you_ want for this feature is
only a small part of our decision on what is needed. Robert's concern
that this might not be useful enough for the general use-cases people
want is a legitimate, if difficult to hear, analysis.

The resistance we get when removing features is sobering. Imagine the
worst case, e.g. xml2, where we rightly implement it later, but there is
something that is better done with the old interface, and we end up
having to support both.

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

+ It's impossible for everything to be true. +

#57Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Bruce Momjian (#56)
Re: Command Triggers

Bruce Momjian <bruce@momjian.us> writes:

We have a big user community and what _you_ want for this feature is
only a small part of our decision on what is needed. Robert's concern
that this might not be useful enough for the general use-cases people
want is a legitimate, if difficult to hear, analysis.

Agreed, his concern is legitimate. Now, I've never been trying to
implement a generic event trigger system and I don't know what it would
take to implement such a beast.

Transaction BEGIN, COMMIT, and ROLLBACK triggers anyone? not me :)

Exploring if my proposal would be a pain to maintain once we have a fully
generic event trigger system someday is legitimate, asking me to design
both the generic event system and the command triggers so that they fit
in is just asking for too much.

The main part of my answer, though, is that all the more complex use
cases involving command triggers that Robert is offering are in fact
possible to implement with what my patch is providing, as soon as you're
ok with understanding the content and format of the nodeToString()
output.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#58Bruce Momjian
bruce@momjian.us
In reply to: Dimitri Fontaine (#57)
Re: Command Triggers

Dimitri Fontaine wrote:

Bruce Momjian <bruce@momjian.us> writes:

We have a big user community and what _you_ want for this feature is
only a small part of our decision on what is needed. Robert's concern
that this might not be useful enough for the general use-cases people
want is a legitimate, if difficult to hear, analysis.

Agreed, his concern is legitimate. Now, I've never been trying to
implement a generic event trigger system and I don't know what it would
take to implement such a beast.

Transaction BEGIN, COMMIT, and ROLLBACK triggers anyone? not me?:)

Exploring if my proposal would be a pain to maintain once we have a fully
generic event trigger system someday is legitimate, asking me to design
both the generic event system and the command triggers so that they fit
in is just asking for too much.

Agreed. I am not against this addition, just pointing out that we have
to be careful.

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

+ It's impossible for everything to be true. +

#59Tom Lane
tgl@sss.pgh.pa.us
In reply to: Dimitri Fontaine (#57)
Re: Command Triggers

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

The main part of my answer, though, is that all the more complex use
cases involving command triggers that Robert is offering are in fact
possible to implement with what my patch is providing, as soon as you're
ok with understanding the content and format of the nodeToString()
output.

Hmm ... I don't think that I *am* ok with that. ISTM that we'd then
find ourselves with any changes in utility statement parse trees
amounting to a user-visible API break, and that's not an acceptable
situation.

We already have this issue of course with respect to C-code add-ons,
but (1) we've established an understanding that people should have to
recompile those for every major release, and (2) changes such as adding
a new field, or even changing an existing field that you don't care
about, don't break C source code. I don't know exactly what you're
imagining that user-written triggers would do with nodeToString strings,
but I'd bet a good lunch that people will use ad-hoc interpretation
methods that are not robust against changes at all. And then they'll
blame us when their triggers break --- not unreasonably, because we
failed to provide a sane API for them to use.

We really need some higher-level API than the raw parse tree, and
I have to admit that I have no idea what that would look like.
But exposing parse trees to user-written triggers is a decision
that we will come to regret, probably as soon as the next release.

regards, tom lane

#60Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Tom Lane (#59)
Re: Command Triggers

Tom Lane <tgl@sss.pgh.pa.us> writes:

Hmm ... I don't think that I *am* ok with that. ISTM that we'd then
find ourselves with any changes in utility statement parse trees
amounting to a user-visible API break, and that's not an acceptable
situation.

Oh, you mean like exposing the parser for syntax coloring etc. I failed
to see it's the same case. Do we have an acceptable proposal on that
front yet?

We already have this issue of course with respect to C-code add-ons,
but (1) we've established an understanding that people should have to
recompile those for every major release, and (2) changes such as adding
a new field, or even changing an existing field that you don't care
about, don't break C source code. I don't know exactly what you're
imagining that user-written triggers would do with nodeToString strings,
but I'd bet a good lunch that people will use ad-hoc interpretation
methods that are not robust against changes at all. And then they'll
blame us when their triggers break --- not unreasonably, because we
failed to provide a sane API for them to use.

Could we offer people a sane API?

Another way could be to bypass BEFORE triggers and let people look at
the catalogs in the AFTER command trigger, and give them the object oid,
name and schemaname for them to do their lookups.

You can still RAISE EXCEPTION in an AFTER command trigger to cancel the
command execution, what you can not do anymore is canceling the command
without killing the current transaction.

We really need some higher-level API than the raw parse tree, and
I have to admit that I have no idea what that would look like.
But exposing parse trees to user-written triggers is a decision
that we will come to regret, probably as soon as the next release.

I was under the illusion that providing users with ready to tweak
examples of robust-against-changes code would cut it. I'd like the
command triggers patch not to depend on designing this API we need.

What do you think of removing the parsetree and the BEFORE trigger
support (so that trigger function can query the catalogs)?

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#61Tom Lane
tgl@sss.pgh.pa.us
In reply to: Dimitri Fontaine (#60)
Re: Command Triggers

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

Tom Lane <tgl@sss.pgh.pa.us> writes:

We really need some higher-level API than the raw parse tree, and
I have to admit that I have no idea what that would look like.
But exposing parse trees to user-written triggers is a decision
that we will come to regret, probably as soon as the next release.

I was under the illusion that providing users with ready to tweak
examples of robust-against-changes code would cut it. I'd like the
command triggers patch not to depend on designing this API we need.

Well, we don't have any such examples, because frankly the nodeToString
representation is pretty damn unfriendly. The only code we have that
does anything with it at all is the readfuncs.c code that turns it back
into trees of C structs, and that's no help for triggers not themselves
written in C. Besides which, readfuncs.c is the poster child for code
that does have to change every time we tweak the struct definitions.
We can't tell people to copy that approach.

What do you think of removing the parsetree and the BEFORE trigger
support (so that trigger function can query the catalogs)?

Well, it gets us out of the business of inventing a suitable API,
but I think it also reduces the feature to a point of near uselessness.
Essentially we'd be saying to trigger authors "something changed, feel
free to inspect the catalogs and see if you can guess what".

Just because the problem is hard doesn't mean you can get away with
not solving it.

regards, tom lane

#62Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Tom Lane (#61)
Re: Command Triggers

Tom Lane <tgl@sss.pgh.pa.us> writes:

Well, we don't have any such examples, because frankly the nodeToString
representation is pretty damn unfriendly. The only code we have that

I tend to agree here, but I know that Jan is convincing enough when he's
saying that it is in fact very friendly.

does anything with it at all is the readfuncs.c code that turns it back
into trees of C structs, and that's no help for triggers not themselves
written in C. Besides which, readfuncs.c is the poster child for code
that does have to change every time we tweak the struct definitions.
We can't tell people to copy that approach.

Providing the same nested C structs thingy in python or perl or tcl
might be feasible and not too sketchy to maintain, but I'm failing to
see how to even approach that for plpgsql.

What do you think of removing the parsetree and the BEFORE trigger
support (so that trigger function can query the catalogs)?

Well, it gets us out of the business of inventing a suitable API,
but I think it also reduces the feature to a point of near uselessness.

Not being generic and flexible is not the same thing as not being of any
use at all. Extension whitelisting is still possible to implement
because all you need to know is the extension's name, then you choose to
let the command string you're given execute or not. Same with
replication or simple auditing cases, you still have the plain command
string to play with.

Not useful enough for being what we ship in 9.2, I can follow you there,
not useful at all, disagreed.

Essentially we'd be saying to trigger authors "something changed, feel
free to inspect the catalogs and see if you can guess what".

No, we'd also be providing the main OID of the object that changed (a
pg_class entry for a CREATE TABLE command, etc), the object name and its
schema name too. And the command string too. ALTER TABLE is still
difficult to handle, other more simple commands might be ok.

Just because the problem is hard doesn't mean you can get away with
not solving it.

That is the single simplest way of handling it, though, so I had to try
that first. Now, maybe we can find the right approach to publishing the
parse tree this month still. Any ideas welcome!

I guess XML would be ok but we don't embed powerful enough tools, and
JSON might be perfect but we would need to have a full blown datatype
and functions to work with that from plpgsql. What other tree-ish data
type can we have?

EXPLAIN is already able to spit out XML and JSON (and YAML) but the
typical client consuming that output is not running as a backend stored
procedure, so I guess that's not a precedent and we still need something
with a good support (type, operators, walking functions…) to back it.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#63Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#62)
Re: Command Triggers

On Sun, Dec 18, 2011 at 5:11 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

I guess XML would be ok but we don't embed powerful enough tools, and
JSON might be perfect but we would need to have a full blown datatype
and functions to work with that from plpgsql.  What other tree-ish data
type can we have?

EXPLAIN is already able to spit out XML and JSON (and YAML) but the
typical client consuming that output is not running as a backend stored
procedure, so I guess that's not a precedent and we still need something
with a good support (type, operators, walking functions…) to back it.

Right. If we're actually going to expose the parse tree, I think JSON
(or even XML) would be a far better way to expose that than the
existing nodeToString() output. Sure, you could make due with the
nodeToString() output for some things, especially in PL/perl or
PL/python. But JSON would be far better, since it's a standard format
rather than something we just made up, and could be used in PL/pgsql
as well, given proper support functions.

Another option would be to do something like this:

CREATE TYPE pg_trigger_on_create_table AS (
catalog_name text,
schema_name text,
relation_name text,
...
);

That's not materially different from exposing the parse tree, but it's
more convenient for PL/pgsql and doesn't require adding a new datatype
like JSON. It might require an awful lot of tuple-construction code
and datatype definitions, though.

Still another option would be to expose some of this information
through "magical" variables or functions, sort of like the way that
declaring a function to return trigger causes it to have NEW and OLD.
It could have STUFF.catalog_name, STUFF.schema_name,
STUFF.relation_name, or whatever we want.

None of these approaches really get around the fact that if the
command syntax changes, the trigger API has to change, too. You might
be able to get around that for CREATE commands by having only AFTER
triggers, and just passing the OID; and for DROP commands by having
only BEFORE triggers, and just passing the OID. But I don't see any
way to make it work very well for ALTER commands.

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

#64Greg Smith
greg@2ndQuadrant.com
In reply to: Robert Haas (#63)
Re: Command Triggers

On 12/18/2011 09:02 PM, Robert Haas wrote:

If we're actually going to expose the parse tree, I think JSON
(or even XML) would be a far better way to expose that than the
existing nodeToString() output. Sure, you could make due with the
nodeToString() output for some things, especially in PL/perl or
PL/python. But JSON would be far better, since it's a standard format
rather than something we just made up, and could be used in PL/pgsql
as well, given proper support functions.

That seems pretty sensible; I wouldn't want to make it a hard
requirement though. There are three major ways this could go for 9.2:

1) Nothing is changed in core, the best that can be done is whatever you
can cram into an extension.

2) 9.2 is released with Command Triggers using nodeToString() output as
the detailed description. That part is labeled "experimental and the
API is expected to change completely in the next release"

3) 9.2 gets enough JSON support to also have an early nodeToJSON API
instead. Now it's labeled "early release and some API changes may be
needed in future releases".

From the perspective of enabling a developer community to spring up
around working in this area, I don't see a huge difference between (2)
and (3). We're going in a direction I don't think is very well explored
yet, by anyone. The idea that we're going to learn enough to accumulate
a comprehensive, stable API before release is rather optimistic.
There's something to be said for Dimitri's suggested approach from the
perspective of "ship it knowing it works for features A, B, then let's
see what else the users think to do with it". We can't determine what
feature complete looks like from what other people are doing anymore;
only way to find out is to see what the world at large thinks of after
release.

Think of it as being like the EXPLAIN plan output. That shipped as just
text, programs popped up to parse it anyway, they suffered some breaks
with each new release. That was still a major win. Then, new easier to
parse formats appeared, making the bar to entry for writing new such
tool much lower. And the construction of a better API for output
benefited from seeing what people had actually been struggling with
before then.

--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.us

#65Alvaro Herrera
alvherre@commandprompt.com
In reply to: Dimitri Fontaine (#60)
Re: Command Triggers

Excerpts from Dimitri Fontaine's message of dom dic 18 16:54:11 -0300 2011:

Tom Lane <tgl@sss.pgh.pa.us> writes:

Hmm ... I don't think that I *am* ok with that. ISTM that we'd then
find ourselves with any changes in utility statement parse trees
amounting to a user-visible API break, and that's not an acceptable
situation.

Oh, you mean like exposing the parser for syntax coloring etc. I failed
to see it's the same case. Do we have an acceptable proposal on that
front yet?

The conclusion that was reached during developer's meeting was that
those interested should use a technique similar to the one used by the
ecpg parser, namely use some sort of tool to transform the gram.y source
code into something else (different productions). That idea is not
useful to you here, I'm afraid.

--
Álvaro Herrera <alvherre@commandprompt.com>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support

#66Robert Haas
robertmhaas@gmail.com
In reply to: Greg Smith (#64)
Re: Command Triggers

On Mon, Dec 19, 2011 at 9:31 AM, Greg Smith <greg@2ndquadrant.com> wrote:

That seems pretty sensible; I wouldn't want to make it a hard requirement
though.  There are three major ways this could go for 9.2:

1) Nothing is changed in core, the best that can be done is whatever you can
cram into an extension.

2) 9.2 is released with Command Triggers using nodeToString() output as the
detailed description.  That part is labeled "experimental and the API is
expected to change completely in the next release"

3) 9.2 gets enough JSON support to also have an early nodeToJSON API
instead.  Now it's labeled "early release and some API changes may be needed
in future releases".

From the perspective of enabling a developer community to spring up around
working in this area, I don't see a huge difference between (2) and (3).

I do. Anyone coding in PL/pgsql is going to find the nodeToString()
output unusable, and we can easily provide a built-in JSON datatype
with enough richness to make that problem go away in time for 9.2.
People in PL/python and PL/perl may be a bit better off, but I see no
reason to ship something for 9.2 and then break it for 9.3 when we
could perfectly well make that compatibility break before the release.
(And, in case it's not clear, yes, I am volunteering to do the work,
if it comes down to that.)

But before we get bogged down in the data representation, I think we
need to make a more fundamental decision: to what extent are we OK
with exporting the parse tree at all, in any form? Tom is arguing
that we shouldn't, and I see his point: the recent DROP command rework
would have broken everybody's command triggers if we had adopted this
proposal, and that would be a real shame, because I don't particularly
like the idea that we can't continue to improve the code and refactor
things because someone out there might be depending on an older and
less well-considered behavior.

On the flip side, I don't really see any other way to make this
feature work at all. I think that AFTER CREATE triggers and BEFORE
DROP triggers could potentially be implemented by just passing OIDs
in, and that might be useful enough for many people. But what about
ALTER? I don't see that you're going to be able to write any sort of
meaningful triggers around ALTER without passing at least some of the
parse tree information to the trigger function.

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

#67Andres Freund
andres@anarazel.de
In reply to: Robert Haas (#66)
Re: Command Triggers

Hi,

On Monday, December 19, 2011 05:12:13 PM Robert Haas wrote:

On Mon, Dec 19, 2011 at 9:31 AM, Greg Smith <greg@2ndquadrant.com> wrote:

That seems pretty sensible; I wouldn't want to make it a hard requirement
though. There are three major ways this could go for 9.2:

1) Nothing is changed in core, the best that can be done is whatever you
can cram into an extension.

2) 9.2 is released with Command Triggers using nodeToString() output as
the detailed description. That part is labeled "experimental and the
API is expected to change completely in the next release"

3) 9.2 gets enough JSON support to also have an early nodeToJSON API
instead. Now it's labeled "early release and some API changes may be
needed in future releases".

From the perspective of enabling a developer community to spring up
around working in this area, I don't see a huge difference between (2)
and (3).

I do. Anyone coding in PL/pgsql is going to find the nodeToString()
output unusable, and we can easily provide a built-in JSON datatype
with enough richness to make that problem go away in time for 9.2.
People in PL/python and PL/perl may be a bit better off, but I see no
reason to ship something for 9.2 and then break it for 9.3 when we
could perfectly well make that compatibility break before the release.
(And, in case it's not clear, yes, I am volunteering to do the work,
if it comes down to that.)

I personally think youre underestimating the complexity of providing a
sensible json compatibility shim ontop the nodestring representation. But I
would like to be proven wrong ;)
Whats your idea?

But before we get bogged down in the data representation, I think we
need to make a more fundamental decision: to what extent are we OK
with exporting the parse tree at all, in any form? Tom is arguing
that we shouldn't, and I see his point: the recent DROP command rework
would have broken everybody's command triggers if we had adopted this
proposal, and that would be a real shame, because I don't particularly
like the idea that we can't continue to improve the code and refactor
things because someone out there might be depending on an older and
less well-considered behavior.

I don't see any realistic way to present the data in way thats abstracted from
the internals for now. The infrastructure is completely new and we don't
really know what it will be used for. I personally have no problem requiring
that any nontrivial nodestring access has to be done via c functions for now.
Building a completely new form of parstree seems way too much effort/flamebait
for me.

Andres

#68Pavel Stehule
pavel.stehule@gmail.com
In reply to: Robert Haas (#66)
Re: Command Triggers

2011/12/19 Robert Haas <robertmhaas@gmail.com>:

On Mon, Dec 19, 2011 at 9:31 AM, Greg Smith <greg@2ndquadrant.com> wrote:

That seems pretty sensible; I wouldn't want to make it a hard requirement
though.  There are three major ways this could go for 9.2:

1) Nothing is changed in core, the best that can be done is whatever you can
cram into an extension.

2) 9.2 is released with Command Triggers using nodeToString() output as the
detailed description.  That part is labeled "experimental and the API is
expected to change completely in the next release"

3) 9.2 gets enough JSON support to also have an early nodeToJSON API
instead.  Now it's labeled "early release and some API changes may be needed
in future releases".

From the perspective of enabling a developer community to spring up around
working in this area, I don't see a huge difference between (2) and (3).

I do.  Anyone coding in PL/pgsql is going to find the nodeToString()
output unusable, and we can easily provide a built-in JSON datatype
with enough richness to make that problem go away in time for 9.2.
People in PL/python and PL/perl may be a bit better off, but I see no
reason to ship something for 9.2 and then break it for 9.3 when we
could perfectly well make that compatibility break before the release.
 (And, in case it's not clear, yes, I am volunteering to do the work,
if it comes down to that.)

absolutelly

Parsing a some document is not a good way for plpgsql. We can enhance
a diagnostics statement for triggers values.

there should be lot of variables, and it is cheap because we can take
content when it is requested

STATEMENT_NAME,
OBJECT_SCHEMA,
OBJECT_NAME,
OBJECT_TYPE,
OBJECT_OID,
ATTRIBUT_NAME,
ATTRIBUT_OID,
ATTRIBUT_TYPE,
STATEMENT_PARAMETERS_ARRAY

plpgsql is not good for recursive data - can be nice if all necessary
data can be flat.

Regards

Pavel

Show quoted text

But before we get bogged down in the data representation, I think we
need to make a more fundamental decision: to what extent are we OK
with exporting the parse tree at all, in any form?  Tom is arguing
that we shouldn't, and I see his point: the recent DROP command rework
would have broken everybody's command triggers if we had adopted this
proposal, and that would be a real shame, because I don't particularly
like the idea that we can't continue to improve the code and refactor
things because someone out there might be depending on an older and
less well-considered behavior.

On the flip side, I don't really see any other way to make this
feature work at all.  I think that AFTER CREATE triggers and BEFORE
DROP triggers could potentially be implemented by just passing OIDs
in, and that might be useful enough for many people.  But what about
ALTER?  I don't see that you're going to be able to write any sort of
meaningful triggers around ALTER without passing at least some of the
parse tree information to the trigger function.

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

#69Robert Haas
robertmhaas@gmail.com
In reply to: Andres Freund (#67)
Re: Command Triggers

On Mon, Dec 19, 2011 at 11:20 AM, Andres Freund <andres@anarazel.de> wrote:

I do.  Anyone coding in PL/pgsql is going to find the nodeToString()
output unusable, and we can easily provide a built-in JSON datatype
with enough richness to make that problem go away in time for 9.2.
People in PL/python and PL/perl may be a bit better off, but I see no
reason to ship something for 9.2 and then break it for 9.3 when we
could perfectly well make that compatibility break before the release.
 (And, in case it's not clear, yes, I am volunteering to do the work,
if it comes down to that.)

I personally think youre underestimating the complexity of providing a
sensible json compatibility shim ontop the nodestring representation. But I
would like to be proven wrong ;)
Whats your idea?

I haven't gotten that far down into the minutiae yet. :-)

But the reason I think it won't be too bad is because the current
representation is darn close to JSON already:

{VAR :varno 2 :varattno 1 :vartype 25 :vartypmod -1 :varcollid 100
:varlevelsup 0 :varnoold 2 :varoattno 1 :location 9998}

=>

{"_":"VAR","varno":2,"varattno":1,"vartype":25,"vartypmod":-1,"varcollid":100,"varlevelsup":0,"varnoold":2,"varoattno":1,"location":9998}

If you've got something like:

{OPEXPR :opno 98 :opfuncid 67 :opresulttype 16 :opretset false
:opcollid 0 :inputcollid 100 :args ({VAR :varno 2 :varattno 1 :vartype
25 :vartypmod -1 :varcollid 100 :varlevelsup 0 :varnoold 2 :varoattno
1 :location 9998} {VAR :varno 1 :varattno 1 :vartype 25 :vartypmod -1
:varcollid 100 :varlevelsup 0 :varnoold 1 :varoattno 1 :location
10009}) :location 10007}

...then the value for the "args" label will just be an object rather
than an integer.

But before we get bogged down in the data representation, I think we
need to make a more fundamental decision: to what extent are we OK
with exporting the parse tree at all, in any form?  Tom is arguing
that we shouldn't, and I see his point: the recent DROP command rework
would have broken everybody's command triggers if we had adopted this
proposal, and that would be a real shame, because I don't particularly
like the idea that we can't continue to improve the code and refactor
things because someone out there might be depending on an older and
less well-considered behavior.

I don't see any realistic way to present the data in way thats abstracted from
the internals for now. The infrastructure is completely new and we don't
really know what it will be used for.

That's my feeling as well, but I'm hoping Tom or someone else has a better idea.

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

#70Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#67)
Re: Command Triggers

Andres Freund <andres@anarazel.de> writes:

On Monday, December 19, 2011 05:12:13 PM Robert Haas wrote:

I do. Anyone coding in PL/pgsql is going to find the nodeToString()
output unusable, and we can easily provide a built-in JSON datatype
with enough richness to make that problem go away in time for 9.2.

I hear the sound of goalposts moving.

I personally think youre underestimating the complexity of providing a
sensible json compatibility shim ontop the nodestring representation. But I
would like to be proven wrong ;)

If we were going to do this at all, the way to do it is to flush the
existing nodestring representation and redefine it as being JSON. No?
If this is as easy as people are claiming, it should not be hard to
revise the lower-level bits of outfuncs/readfuncs to make the text
representation compatible. And there's no reason we can't change
what is stored in pg_rewrite.

But before we get bogged down in the data representation, I think we
need to make a more fundamental decision: to what extent are we OK
with exporting the parse tree at all, in any form? Tom is arguing
that we shouldn't, and I see his point: the recent DROP command rework
would have broken everybody's command triggers if we had adopted this
proposal, and that would be a real shame, because I don't particularly
like the idea that we can't continue to improve the code and refactor
things because someone out there might be depending on an older and
less well-considered behavior.

The problem that changing the nodestring representation could help with
is making user-written triggers roughly as robust as C code is, to wit
that you don't have to change it as long as the specific fields it
touches aren't redefined. The question is whether that's good enough.
The DROP command changes provide a pretty strong clue that it isn't.
Admittedly, that's not the sort of change we make too often. But I
will be seriously annoyed if we start getting the sort of pushback
on parsetree changes that we've been hearing from certain quarters about
configuration file changes. Those structures are *internal* and we have
got to have the flexibility to whack them around.

regards, tom lane

#71Greg Smith
greg@2ndQuadrant.com
In reply to: Robert Haas (#66)
Re: Command Triggers

On 12/19/2011 11:12 AM, Robert Haas wrote:

But before we get bogged down in the data representation, I think we
need to make a more fundamental decision: to what extent are we OK
with exporting the parse tree at all, in any form? Tom is arguing
that we shouldn't, and I see his point: the recent DROP command rework
would have broken everybody's command triggers if we had adopted this
proposal, and that would be a real shame, because I don't particularly
like the idea that we can't continue to improve the code and refactor
things because someone out there might be depending on an older and
less well-considered behavior.

Any interface here would need to be in the same sense Linux uses the
term: subject to change in every major version, and maybe even in a
minor one if that's the best way to solve a higher priority issue. An
example we've been consuming that comes to mind is the "API" for keeping
processes from being killed by the OOM killer. It's far from stable:
http://archives.postgresql.org/message-id/4CE5E437.7080902@2ndquadrant.com
but it's still possible for users of it to keep up with new releases,
and when feasible some work toward backward compatibility is done (but
not always)

As a tool author, I would expect anything working at the level where the
data needed is only available from the parse tree would need to be
re-tested against each new version, and then have version specific
changes as needed. Setting the expectations bar any higher for
consumers of that interface would be unrealistic. The minority of
people who'd like to use this feature shouldn't want to see PostgreSQL
development hamstrung for the majority either, and the standards for
breakage here should be clear from the beginning--unlike those for, say,
GUC changes between releases.

--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.us

#72Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#70)
Re: Command Triggers

On Mon, Dec 19, 2011 at 12:52 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I personally think youre underestimating the complexity of providing a
sensible json compatibility shim ontop the nodestring representation. But I
would like to be proven wrong ;)

If we were going to do this at all, the way to do it is to flush the
existing nodestring representation and redefine it as being JSON.  No?
If this is as easy as people are claiming, it should not be hard to
revise the lower-level bits of outfuncs/readfuncs to make the text
representation compatible.  And there's no reason we can't change
what is stored in pg_rewrite.

I thought about that. A quick inspection suggests that this would
slightly increase the size of the stored rules, because the node type
would need to become a key, which would add at least a few more
characters, and also because we'd need more quoting. That is:

{THING :wump 1}

becomes, at a minimum:

{"_": "THING", "wump": 1}

And that's using a somewhat-lame single character label for the node
tag. Now, I suspect that in practice the cost of the stored rules
becoming slightly larger is negligible, so maybe it's worth it.

But before we get bogged down in the data representation, I think we
need to make a more fundamental decision: to what extent are we OK
with exporting the parse tree at all, in any form?  Tom is arguing
that we shouldn't, and I see his point: the recent DROP command rework
would have broken everybody's command triggers if we had adopted this
proposal, and that would be a real shame, because I don't particularly
like the idea that we can't continue to improve the code and refactor
things because someone out there might be depending on an older and
less well-considered behavior.

The problem that changing the nodestring representation could help with
is making user-written triggers roughly as robust as C code is, to wit
that you don't have to change it as long as the specific fields it
touches aren't redefined.  The question is whether that's good enough.

Agreed.

The DROP command changes provide a pretty strong clue that it isn't.
Admittedly, that's not the sort of change we make too often.  But I
will be seriously annoyed if we start getting the sort of pushback
on parsetree changes that we've been hearing from certain quarters about
configuration file changes.  Those structures are *internal* and we have
got to have the flexibility to whack them around.

Yes.

Maybe we should try to split the baby here and defer the question of
whether to expose any of the parse tree internals, and if so how much,
to a future release. It seems to me that we could design a fairly
useful set of functionality around AFTER-CREATE, BEFORE-DROP, and
maybe even AFTER-ALTER triggers without exposing any parse tree
details. For CREATE and ALTER, that would make it the client's
responsibility to inspect the system catalogs and figure out what had
happened and what to do about it, which is admittedly not ideal, but
it would be more than we have now, and it would then give us the
option to consider requests to expose more information in future
releases on a case-by-case basis, rather than making a blanket
decision about whether to expose the parse tree or not. I have a
sneaking suspicion that, while we probably can't get by without
exposing any parse tree information ever, the amount we truly need to
expose might end up being only a small percentage of the total...

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

#73Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#72)
Re: Command Triggers

Hi,

Robert Haas <robertmhaas@gmail.com> writes:

Maybe we should try to split the baby here and defer the question of
whether to expose any of the parse tree internals, and if so how much,
to a future release. It seems to me that we could design a fairly
useful set of functionality around AFTER-CREATE, BEFORE-DROP, and
maybe even AFTER-ALTER triggers without exposing any parse tree
details.

+1

Also remember that you have a “normalized” command string to play with.
Lots of use cases are already ok here. The other ones would need a tree
representation that's easy to consume, which in the current state of
affairs (I saw no progress on the JSON data type and facilities) is very
hard to imagine when you consider PLpgSQL.

So unless I hear about a show stopper, I'm going to work some more on
the command trigger patch where I still had some rough edges to polish.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#74Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#72)
1 attachment(s)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

Maybe we should try to split the baby here and defer the question of
whether to expose any of the parse tree internals, and if so how much,
to a future release. It seems to me that we could design a fairly
useful set of functionality around AFTER-CREATE, BEFORE-DROP, and
maybe even AFTER-ALTER triggers without exposing any parse tree
details.

Please find attached v5 of the patch, with nodeToString() support removed.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

Attachments:

command-trigger.v5.patch.gzapplication/octet-streamDownload
#75Andres Freund
andres@anarazel.de
In reply to: Robert Haas (#72)
Re: Command Triggers

On Monday, December 19, 2011 07:37:46 PM Robert Haas wrote:

Maybe we should try to split the baby here and defer the question of
whether to expose any of the parse tree internals, and if so how much,
to a future release.

I personally think this is an error and those details should at least be
available on the c level (e.g. some pg_command_trigger_get_plan() function,
only available via C) to allow sensible playing around with that knowledge. I
don't really see making progress towards a nice interface unless we get
something to play around with out there.

Andres

#76Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Andres Freund (#75)
Re: Command Triggers

Andres Freund <andres@anarazel.de> writes:

I personally think this is an error and those details should at least be
available on the c level (e.g. some pg_command_trigger_get_plan() function,
only available via C) to allow sensible playing around with that knowledge. I
don't really see making progress towards a nice interface unless we get
something to play around with out there.

If you target C coded triggers then all you need to do is provide a
pointer to the Node *parsetree, I would think. What else?

The drawback though is still the same, the day you do that you've
proposed a public API and changing the parsetree stops being internal
refactoring. The way around this problem is that if you want a command
trigger in C, just write an extension that implements the Process
Utility hook. Bonus, you can have that working with already released
versions of PostgreSQL.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#77Andres Freund
andres@anarazel.de
In reply to: Dimitri Fontaine (#76)
Re: Command Triggers

On Friday, January 13, 2012 11:53:32 PM Dimitri Fontaine wrote:

Andres Freund <andres@anarazel.de> writes:

I personally think this is an error and those details should at least be
available on the c level (e.g. some pg_command_trigger_get_plan()
function, only available via C) to allow sensible playing around with
that knowledge. I don't really see making progress towards a nice
interface unless we get something to play around with out there.

If you target C coded triggers then all you need to do is provide a
pointer to the Node *parsetree, I would think. What else?

Yes.

Being able to turn that into a statement again is still valuable imo.

The drawback though is still the same, the day you do that you've
proposed a public API and changing the parsetree stops being internal
refactoring.

Yes, sure. I don't particularly care though actually. Changing some generic
guts of trigger functions is not really that much of a problem compared to the
other stuff involoved in a version migration.

The way around this problem is that if you want a command
trigger in C, just write an extension that implements the Process
Utility hook.

The point is that with CREATE COMMAND TRIGGER only the internal part of the
triggers need to change. No the external interface. Which is a big difference
from my pov.
Also hooks are relatively hard to stack, i.e. its hard to use them sensibly
from multiple independent projects. They also cannot be purely installed via
extensions/sql.

Andres

#78Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Andres Freund (#77)
Re: Command Triggers

Andres Freund <andres@anarazel.de> writes:

If you target C coded triggers then all you need to do is provide a
pointer to the Node *parsetree, I would think. What else?

Yes.

Being able to turn that into a statement again is still valuable imo.

That part of the WIP code is still in the patch, yes.

The drawback though is still the same, the day you do that you've
proposed a public API and changing the parsetree stops being internal
refactoring.

Yes, sure. I don't particularly care though actually. Changing some generic
guts of trigger functions is not really that much of a problem compared to the
other stuff involoved in a version migration.

Let's hear about it from Tom, who's mainly been against publishing that.

The point is that with CREATE COMMAND TRIGGER only the internal part of the
triggers need to change. No the external interface. Which is a big difference
from my pov.

I'm not sure. The way you get the arguments would stay rather stable,
but the parsetree would change at each release: that's not a long term
API here. I fail to see much difference in between a hook and a command
trigger as soon as you've chosen to implement the feature in C.

Also hooks are relatively hard to stack, i.e. its hard to use them sensibly
from multiple independent projects. They also cannot be purely installed via
extensions/sql.

That remains true, you can't easily know in which order your hooks will
get fired, contrary to triggers, and you can't even list the hooks.

I fear that we won't be able to answer your need here in 9.2 though.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#79Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#76)
Re: Command Triggers

On Fri, Jan 13, 2012 at 5:53 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Andres Freund <andres@anarazel.de> writes:

I personally think this is an error and those details should at least be
available on the c level (e.g. some pg_command_trigger_get_plan() function,
only available via C) to allow sensible playing around with that knowledge. I
don't really see making progress towards a nice interface unless we get
something to play around with out there.

If you target C coded triggers then all you need to do is provide a
pointer to the Node *parsetree, I would think.  What else?

The drawback though is still the same, the day you do that you've
proposed a public API and changing the parsetree stops being internal
refactoring.  The way around this problem is that if you want a command
trigger in C, just write an extension that implements the Process
Utility hook.  Bonus, you can have that working with already released
versions of PostgreSQL.

But on the flip side, I think we're generally a bit more flexible
about exposing things via C than through the procedural languages. I
think it's reasonable for people to complain about their PL/pgsql
functions breaking between major releases, but I have less sympathy
for someone who has the same problem when coding in C. When you
program in C, you're interfacing with the guts of the system, and we
can't improve the system without changing the guts.

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

#80Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#79)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

But on the flip side, I think we're generally a bit more flexible
about exposing things via C than through the procedural languages.

So we could still expose the parsetree of the current command. I wonder
if it's already possible to get that from a C coded trigger, but I'll
admit I'm yet to code a trigger in C. Will look into that.

Then as Andres proposed, a new function would be available to get the
value, we're not changing the trigger procedure function API in case the
language is C…

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#81Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Dimitri Fontaine (#80)
Re: Command Triggers

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

Robert Haas <robertmhaas@gmail.com> writes:

But on the flip side, I think we're generally a bit more flexible
about exposing things via C than through the procedural languages.

Then as Andres proposed, a new function would be available to get the
value, we're not changing the trigger procedure function API in case the
language is C…

I've been updating my github branch with a patch that provides the
parsetree to C coded command trigger functions only, as their 5th
argument, of type INTERNAL (so that only C coded procs apply).

https://github.com/dimitri/postgres/compare/master...command_triggers

I still have some cleaning to do before to prepare the next patch
version, such as documentation updating and dealing with rewrites of
CHECK and DEFAULT column constraints in CREATE TABLE. I had to add
support for the T_A_Const parser node, and now I'm about to see about
adding support for the T_A_Expr one, but I can't help to wonder how the
rewriter could work without them.

What is this simple thing I'm missing here, if any?

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#82Tom Lane
tgl@sss.pgh.pa.us
In reply to: Dimitri Fontaine (#81)
Re: Command Triggers

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

I still have some cleaning to do before to prepare the next patch
version, such as documentation updating and dealing with rewrites of
CHECK and DEFAULT column constraints in CREATE TABLE. I had to add
support for the T_A_Const parser node, and now I'm about to see about
adding support for the T_A_Expr one, but I can't help to wonder how the
rewriter could work without them.

It doesn't, and it shouldn't have to. If those nodes get to the
rewriter then somebody forgot to apply parse analysis. What's your test
case?

regards, tom lane

#83Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Tom Lane (#82)
Re: Command Triggers

Tom Lane <tgl@sss.pgh.pa.us> writes:

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

I still have some cleaning to do before to prepare the next patch
version, such as documentation updating and dealing with rewrites of
CHECK and DEFAULT column constraints in CREATE TABLE. I had to add
support for the T_A_Const parser node, and now I'm about to see about
adding support for the T_A_Expr one, but I can't help to wonder how the
rewriter could work without them.

It doesn't, and it shouldn't have to. If those nodes get to the
rewriter then somebody forgot to apply parse analysis. What's your test
case?

I'm trying to rewrite the command string from the parse tree, and the
simple example that I use to raise an ERROR is the following:

create table foo (id serial, foo integer default 1, primary key(id));

I don't know if the parsetree handed by ProcessUtility() has gone
through parse analysis and I don't know if that's needed to execute the
commands, so maybe I have some high level function to call before
walking the parse tree in my new rewriting support?

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#84Tom Lane
tgl@sss.pgh.pa.us
In reply to: Dimitri Fontaine (#83)
Re: Command Triggers

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

Tom Lane <tgl@sss.pgh.pa.us> writes:

It doesn't, and it shouldn't have to. If those nodes get to the
rewriter then somebody forgot to apply parse analysis. What's your test
case?

I'm trying to rewrite the command string from the parse tree, and the
simple example that I use to raise an ERROR is the following:

create table foo (id serial, foo integer default 1, primary key(id));

That needs to go through transformCreateStmt(). The comments at the
head of parse_utilcmd.c might be informative.

While I've not looked at your patch, I can't escape the suspicion that
this means you are trying to do the wrong things in the wrong places.
Calling transformCreateStmt() from some random place is not going to
make things better; it is going to break CREATE TABLE, which expects to
do that for itself.

regards, tom lane

#85Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Tom Lane (#84)
Re: Command Triggers

Tom Lane <tgl@sss.pgh.pa.us> writes:

create table foo (id serial, foo integer default 1, primary key(id));

That needs to go through transformCreateStmt(). The comments at the
head of parse_utilcmd.c might be informative.

Indeed, thanks for the heads up here.

While I've not looked at your patch, I can't escape the suspicion that
this means you are trying to do the wrong things in the wrong places.
Calling transformCreateStmt() from some random place is not going to
make things better; it is going to break CREATE TABLE, which expects to
do that for itself.

From the comments in the file, it seems like I could either call the
function where I need it on a copy of the parse tree (as made by the
copyObject() function), or reshuffle either when that call happen or
when the calling of the command trigger user functions happens.

At the moment the trigger functions are called from
standard_ProcessUtility() and are given the parse tree as handed over to
that function, before the parse analysis.

We can easily enough copy the parse tree and do another round of parse
analysis on it only when some command triggers are going to get called.
Is the cost of doing so acceptable?

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#86Alvaro Herrera
alvherre@commandprompt.com
In reply to: Dimitri Fontaine (#85)
Re: Command Triggers

Excerpts from Dimitri Fontaine's message of mié ene 18 16:03:29 -0300 2012:

At the moment the trigger functions are called from
standard_ProcessUtility() and are given the parse tree as handed over to
that function, before the parse analysis.

We can easily enough copy the parse tree and do another round of parse
analysis on it only when some command triggers are going to get called.
Is the cost of doing so acceptable?

Huh, isn't it simpler to just pass the triggers the parse tree *after*
parse analysis? I don't understand what you're doing here.

--
Álvaro Herrera <alvherre@commandprompt.com>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support

#87Andres Freund
andres@anarazel.de
In reply to: Alvaro Herrera (#86)
Re: Command Triggers

On Wednesday, January 18, 2012 08:17:36 PM Alvaro Herrera wrote:

Excerpts from Dimitri Fontaine's message of mié ene 18 16:03:29 -0300 2012:

At the moment the trigger functions are called from
standard_ProcessUtility() and are given the parse tree as handed over to
that function, before the parse analysis.

We can easily enough copy the parse tree and do another round of parse
analysis on it only when some command triggers are going to get called.
Is the cost of doing so acceptable?

Huh, isn't it simpler to just pass the triggers the parse tree *after*
parse analysis? I don't understand what you're doing here.

Parse analysis is not exactly nicely separated for utility statements.

Andres

#88Tom Lane
tgl@sss.pgh.pa.us
In reply to: Dimitri Fontaine (#85)
Re: Command Triggers

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

We can easily enough copy the parse tree and do another round of parse
analysis on it only when some command triggers are going to get called.
Is the cost of doing so acceptable?

It's not the costs I'm worried about so much as the side effects ---
locks and so forth. Also, things like assignment of specific names
for indexes and sequences seem rather problematic. In the worst case
the trigger could run seeing "foo_bar_idx1" as the name of an index
to be created, and then when the action actually happens, the name
turns out to be "foo_bar_idx2" because someone else took the first name
meanwhile.

As I said, I think this suggests that you're trying to do the triggers
in the wrong place.

regards, tom lane

#89Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#88)
Re: Command Triggers

On Wednesday, January 18, 2012 08:31:49 PM Tom Lane wrote:

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

We can easily enough copy the parse tree and do another round of parse
analysis on it only when some command triggers are going to get called.
Is the cost of doing so acceptable?

It's not the costs I'm worried about so much as the side effects ---
locks and so forth. Also, things like assignment of specific names
for indexes and sequences seem rather problematic. In the worst case
the trigger could run seeing "foo_bar_idx1" as the name of an index
to be created, and then when the action actually happens, the name
turns out to be "foo_bar_idx2" because someone else took the first name
meanwhile.

You can't generally assume such a thing anyway. Remember there can be BEFORE
command triggers. It would be easy to create a conflict there.

The CREATE TABLE will trigger command triggers on CREATE SEQUENCE and ALTER
SEQUENCE while creating the table. If one actually wants to do anything about
those that would be the place.

As I said, I think this suggests that you're trying to do the triggers
in the wrong place.

In my opinion it mostly shows that parse analysis of utlity statments is to
intermingled with other stuff.... Not sure what to do about that.

Andres

#90Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Alvaro Herrera (#86)
Re: Command Triggers

Alvaro Herrera <alvherre@commandprompt.com> writes:

Huh, isn't it simpler to just pass the triggers the parse tree *after*
parse analysis? I don't understand what you're doing here.

I didn't realize that the parse analysis is in fact done from within
standard_ProcessUtility() directly, which means your suggestion is
indeed workable.

Tom Lane <tgl@sss.pgh.pa.us> writes:

It's not the costs I'm worried about so much as the side effects ---

Ok, so I'm now calling the command trigger procedures once the parse
analysis is done, and guess what, I'm back to the same problem as
before:

https://github.com/dimitri/postgres/commit/4bfab6344a554c09f7322e861f9d09468f641bd9

CREATE TABLE public."ab_foo-bar"
(
id serial NOT NULL,
foo integer default 1,
PRIMARY KEY(id)
);
NOTICE: CREATE TABLE will create implicit sequence "ab_foo-bar_id_seq" for serial column "ab_foo-bar.id"
NOTICE: snitch: CREATE SEQUENCE
ERROR: unrecognized node type: 904

I'm not sure about the next step, and I'm quite sure I need to stop here
for tonight. Any advice welcome, I'll be working on that again as soon
as tomorrow.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#91Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#90)
Re: Command Triggers

On Wed, Jan 18, 2012 at 4:23 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Alvaro Herrera <alvherre@commandprompt.com> writes:

Huh, isn't it simpler to just pass the triggers the parse tree *after*
parse analysis?  I don't understand what you're doing here.

I didn't realize that the parse analysis is in fact done from within
standard_ProcessUtility() directly, which means your suggestion is
indeed workable.

Tom Lane <tgl@sss.pgh.pa.us> writes:

It's not the costs I'm worried about so much as the side effects ---

Ok, so I'm now calling the command trigger procedures once the parse
analysis is done, and guess what, I'm back to the same problem as
before:

 https://github.com/dimitri/postgres/commit/4bfab6344a554c09f7322e861f9d09468f641bd9

 CREATE TABLE public."ab_foo-bar"
 (
   id serial NOT NULL,
   foo integer default 1,
   PRIMARY KEY(id)
 );
 NOTICE:  CREATE TABLE will create implicit sequence "ab_foo-bar_id_seq" for serial column "ab_foo-bar.id"
 NOTICE:  snitch: CREATE SEQUENCE
 ERROR:  unrecognized node type: 904

I'm not sure about the next step, and I'm quite sure I need to stop here
for tonight. Any advice welcome, I'll be working on that again as soon
as tomorrow.

My advice is to forget about trying to provide the command string to
the user for the first version of this patch. As you're finding out,
there's no simple, easy, obvious way of doing it, and there are N>0
useful things that can be done without that functionality. I continue
to think that for a first version of this, we should be satisfied to
pass just the OID. I know that's not really what you want, but
there's much to be said for picking a goal that is achievable in the
limited time available, and I fear that setting your sights too high
will lead to something that either (a) doesn't get committed, or (b)
gets committed, but turns out not to work very well, either of which
would be less than ideal.

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

#92Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#91)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

My advice is to forget about trying to provide the command string to
the user for the first version of this patch. As you're finding out,
there's no simple, easy, obvious way of doing it, and there are N>0
useful things that can be done without that functionality.

Actually, none of my current examples and tests are relying on it. For
the trigger based replications Jan said he would need the full parse
tree rather than just the command string anyway, so we're not loosing
anything more that we already were ready to postpone.

I continue
to think that for a first version of this, we should be satisfied to
pass just the OID. I know that's not really what you want, but
there's much to be said for picking a goal that is achievable in the
limited time available, and I fear that setting your sights too high
will lead to something that either (a) doesn't get committed, or (b)
gets committed, but turns out not to work very well, either of which
would be less than ideal.

Agreed, mostly.

From the code I already have I think we should still pass in the command
tag, the schema name (or null) and the object name (or null) as input
parameters to the trigger's procedure.

I'm now working on that and then the concurrency angle of the command
triggers.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#93Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#92)
Re: Command Triggers

On Fri, Jan 20, 2012 at 9:28 AM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

My advice is to forget about trying to provide the command string to
the user for the first version of this patch.  As you're finding out,
there's no simple, easy, obvious way of doing it, and there are N>0
useful things that can be done without that functionality.

Actually, none of my current examples and tests are relying on it. For
the trigger based replications Jan said he would need the full parse
tree rather than just the command string anyway, so we're not loosing
anything more that we already were ready to postpone.

Cool.

 I continue
to think that for a first version of this, we should be satisfied to
pass just the OID.  I know that's not really what you want, but
there's much to be said for picking a goal that is achievable in the
limited time available, and I fear that setting your sights too high
will lead to something that either (a) doesn't get committed, or (b)
gets committed, but turns out not to work very well, either of which
would be less than ideal.

Agreed, mostly.

From the code I already have I think we should still pass in the command
tag, the schema name (or null) and the object name (or null) as input
parameters to the trigger's procedure.

I think the OID is better than the name, but if it's easy to pass the
name and schema, then I'm fine with it. But I do think this is one of
those problems that is complex enough that we should be happy to get
the core infrastructure in in the first commit, even with an extremely
limited amount of functionality, and then build on it.
Enhance-and-extend is so much easier than a monolithic code drop.

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

#94Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#93)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

I think the OID is better than the name, but if it's easy to pass the
name and schema, then I'm fine with it. But I do think this is one of

It's quite easy to get name and schema from the command yes, here's an
example of how I'm doing it for some commands:

case T_CreateStmt:
{
CreateStmt *node = (CreateStmt *)parsetree;
cmd->schemaname = RangeVarGetNamespace(node->relation);
cmd->objectname = node->relation->relname;
break;
}

case T_AlterTableStmt:
{
AlterTableStmt *node = (AlterTableStmt *)parsetree;
cmd->schemaname = RangeVarGetNamespace(node->relation);
cmd->objectname = node->relation->relname;
break;
}

case T_CreateExtensionStmt:
{
cmd->schemaname = NULL;
cmd->objectname = ((CreateExtensionStmt *)parsetree)->extname;
break;
}

Getting the OID on the other hand is much harder, because each command
implements that part as it wants to, and DefineWhatever() functions are
returning void. We could maybe have them return the main Oid of the
object created, or we could have the CommandContext structure I'm using
be a backend global variable that any command would stuff.

In any case, adding support for the OID only works for after trigger
when talking about CREATE commands and for before trigger if talking
about DROP commands, assuming that the trigger procedure is run after
we've been locating said Oid.

It seems to me to be a couple orders of magnitude more work to get the
Oid here compared to just get the schemaname and objectname. And getting
those works in all cases as we take them from the command itself (we
fill in the schema with the first search_path entry when it's not given
explicitly in the command)

CREATE TABLE foo();
NOTICE: tag: CREATE TABLE
NOTICE: enforce_local_style{public.foo}: foo

those problems that is complex enough that we should be happy to get
the core infrastructure in in the first commit, even with an extremely
limited amount of functionality, and then build on it.
Enhance-and-extend is so much easier than a monolithic code drop.

I'm happy to read that, and I'm preparing next patch version (v6) with
that goal in mind.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#95Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#94)
Re: Command Triggers

On Fri, Jan 20, 2012 at 12:14 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

I think the OID is better than the name, but if it's easy to pass the
name and schema, then I'm fine with it.  But I do think this is one of

It's quite easy to get name and schema from the command yes, here's an
example of how I'm doing it for some commands:

               case T_CreateStmt:
               {
                       CreateStmt *node = (CreateStmt *)parsetree;
                       cmd->schemaname = RangeVarGetNamespace(node->relation);
                       cmd->objectname = node->relation->relname;
                       break;
               }

               case T_AlterTableStmt:
               {
                       AlterTableStmt *node = (AlterTableStmt *)parsetree;
                       cmd->schemaname = RangeVarGetNamespace(node->relation);
                       cmd->objectname = node->relation->relname;
                       break;
               }

               case T_CreateExtensionStmt:
               {
                       cmd->schemaname = NULL;
                       cmd->objectname = ((CreateExtensionStmt *)parsetree)->extname;
                       break;
               }

Getting the OID on the other hand is much harder, because each command
implements that part as it wants to, and DefineWhatever() functions are
returning void. We could maybe have them return the main Oid of the
object created, or we could have the CommandContext structure I'm using
be a backend global variable that any command would stuff.

In any case, adding support for the OID only works for after trigger
when talking about CREATE commands and for before trigger if talking
about DROP commands, assuming that the trigger procedure is run after
we've been locating said Oid.

It seems to me to be a couple orders of magnitude more work to get the
Oid here compared to just get the schemaname and objectname. And getting
those works in all cases as we take them from the command itself (we
fill in the schema with the first search_path entry when it's not given
explicitly in the command)

 CREATE TABLE foo();
 NOTICE:  tag:  CREATE TABLE
 NOTICE:  enforce_local_style{public.foo}: foo

Hmm, OK. But what happens if the user doesn't specify a schema name explicitly?

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

#96Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#95)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

Hmm, OK. But what happens if the user doesn't specify a schema name explicitly?

For the creation case, RangeVarGetCreationNamespace should handle that.
The code Dimitri quoted is wrong, but not that hard to fix.

Unfortunately, the code he quoted for the ALTER case is also wrong,
and harder to fix. Until you've done the lookup you don't know which
schema the referenced object is in. And I don't care for the idea of
doing the lookup twice, as (a) it'll be slower and (b) there are race
cases where it will give the wrong answer, ie return an object other
than the one the ALTER eventually acts on.

Really I think there is not any single point where you can put the
command-trigger hook and be done. In almost every case, the right
place is going to be buried somewhere within the execution of the
command.

regards, tom lane

#97Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Tom Lane (#96)
Re: Command Triggers

Tom Lane <tgl@sss.pgh.pa.us> writes:

For the creation case, RangeVarGetCreationNamespace should handle that.
The code Dimitri quoted is wrong, but not that hard to fix.

Ok.

Unfortunately, the code he quoted for the ALTER case is also wrong,
and harder to fix. Until you've done the lookup you don't know which
schema the referenced object is in. And I don't care for the idea of
doing the lookup twice, as (a) it'll be slower and (b) there are race
cases where it will give the wrong answer, ie return an object other
than the one the ALTER eventually acts on.

Oh. Yes.

Really I think there is not any single point where you can put the
command-trigger hook and be done. In almost every case, the right
place is going to be buried somewhere within the execution of the
command.

I'm finally realizing it. I already introduced a structure called
CommandContextData to keep track of the current command elements we want
to pass down to command triggers, I think we're saying that this should
be a static variable that commands will need to be editing.

The main problem with that approach is that we will need to spread
calling the command trigger entry points to every supported command, I
wanted to avoid that first. It's no big deal though, as the API is
simple enough.

Expect a new patch made this way early next week.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#98Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Dimitri Fontaine (#97)
1 attachment(s)
Re: Command Triggers

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

Really I think there is not any single point where you can put the
command-trigger hook and be done. In almost every case, the right
place is going to be buried somewhere within the execution of the
command.

I'm finally realizing it. I already introduced a structure called
CommandContextData to keep track of the current command elements we want
to pass down to command triggers, I think we're saying that this should
be a static variable that commands will need to be editing.

In fact passing it as an argument to the command trigger API is much
simpler and done in the attached. I'm having problems here with my
install and not enough time this week (you don't speak English if you
don't use understatements here and there, right?) so please expect a
one-step-further patch to show the integration concept, not a ready for
commit one just yet.

Next steps are about adding support for more commands, and now that we
have settled on a simpler integration that will be easy. The parameters
sent to the trigger procedure are now the command tag, the main object
Oid, its schema name and its object name. Only the command tag will
never be NULL, all the other columns could be left out when calling an
ANY COMMAND trigger, or a command on a schemaless object.

Note: the per-command integration means the Oid is generally available,
so let's just export it.

An ack about the way it's now implemented would be awesome, and we could
begin to start about which set of command exactly we want supported from
the get go (default to all of them of course, but well, I don't think
that's necessarily the best use of our time given our command list).

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

Attachments:

command-trigger.v6.patch.gzapplication/octet-streamDownload
#99Simon Riggs
simon@2ndQuadrant.com
In reply to: Dimitri Fontaine (#98)
Re: Command Triggers

On Thu, Jan 26, 2012 at 10:00 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

Really I think there is not any single point where you can put the
command-trigger hook and be done.  In almost every case, the right
place is going to be buried somewhere within the execution of the
command.

I'm finally realizing it. I already introduced a structure called
CommandContextData to keep track of the current command elements we want
to pass down to command triggers, I think we're saying that this should
be a static variable that commands will need to be editing.

In fact passing it as an argument to the command trigger API is much
simpler and done in the attached. I'm having problems here with my
install and not enough time this week (you don't speak English if you
don't use understatements here and there, right?) so please expect a
one-step-further patch to show the integration concept, not a ready for
commit one just yet.

Next steps are about adding support for more commands, and now that we
have settled on a simpler integration that will be easy. The parameters
sent to the trigger procedure are now the command tag, the main object
Oid, its schema name and its object name. Only the command tag will
never be NULL, all the other columns could be left out when calling an
ANY COMMAND trigger, or a command on a schemaless object.

Note: the per-command integration means the Oid is generally available,
so let's just export it.

An ack about the way it's now implemented would be awesome, and we could
begin to start about which set of command exactly we want supported from
the get go (default to all of them of course, but well, I don't think
that's necessarily the best use of our time given our command list).

Looks good so far.

A user centric review of this patch...

Would like a way to say ALL COMMANDS rather than write out list of
supported commands. That list is presumably subject to change, so not
having this could result in bugs in later code.

The example given in the docs has no explanation. More examples and
explanation please. Would specifically be interested in a command
trigger which simply prevents all commands, which has many uses and is
simple enough to be in docs.

Why do we exclude SEQUENCEs? That could well be a problem for intended users

Please explain/add to docs/comments why ALTER VIEW is not supported?
Why not other commands??
Not a problem, but users will ask, so we should write down the answer

Please document why pg_cmdtriggers is a separate table and not
enhancement of pg_triggers?

Thanks

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

#100Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Simon Riggs (#99)
1 attachment(s)
Re: Command Triggers

Hi,

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

An ack about the way it's now implemented would be awesome

I'm still missing that, which is only fair, just a ping from me here.
This is about where to hook in the command trigger calls and which
parameters do we call the procedures with. I've now done it the way Tom
hinted me to, so I guess it's ok.

Would like a way to say ALL COMMANDS rather than write out list of
supported commands. That list is presumably subject to change, so not
having this could result in bugs in later code.

That was included in the patch, but docs were not updated, they are now.

The example given in the docs has no explanation. More examples and
explanation please. Would specifically be interested in a command
trigger which simply prevents all commands, which has many uses and is
simple enough to be in docs.

Done. Docs still need some more love before commit, I'm planning to be
doing that tomorrow and/or the next days.

Why do we exclude SEQUENCEs? That could well be a problem for intended users

Please explain/add to docs/comments why ALTER VIEW is not supported?
Why not other commands??
Not a problem, but users will ask, so we should write down the answer

I've added support for much more commands (49 of them or something,
including SEQUENCEs), will continue doing so tomorrow, I think we should
be ok to support any and all commands that make sense for 9.2.

I still have some basic CREATE commands to add support for, then CLUSTER
and VACUUM and REINDEX, then it's down to ALTER commands where only
ALTER TABLE is currently finished. That's almost mechanical editing
here, the brain only gets used to pick the right place to hook-in.

The other change I need to make is support for regression tests. I
guess we will want one test per command included. The tests will need
to be in their own file and excluded from the parallel schedule too, or
we would be changing the behavior of the other tests under them and
couldn't compare the outputs anymore.

Please document why pg_cmdtriggers is a separate table and not
enhancement of pg_triggers?

Because pg_triggers is made for triggers attached to a relation, as we
can see from that unique constraint:

"pg_trigger_tgrelid_tgname_index" UNIQUE, btree (tgrelid, tgname)

A command trigger is attached to a command rather than a relation, so it
needs to live in its own place:

"pg_cmdtrigger_ctgcommand_ctgname_index" UNIQUE, btree (ctgcommand, ctgname)

I'm not sure how and where to include that in the docs, any advice would
be appreciated.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

Attachments:

command-trigger.v7.patch.gzapplication/octet-streamDownload
���:Ocommand-trigger.v7.patch�<iw�F��������q����v|��abcp��������Dw����{oUI%!�^f�|�O��j�u��[����������U���a����Y�
��I8�9*���
+8���3�}QoY�J�m��/^4X�V;o6����W(�J�������E��`%�h�4t�Fgl���{?����`�~e�a������/]������r�0����3��ccx|��~Y��\��f?~kX1w��`Z���+��l'r|/
���_ �'���=��F�qh��h��,8�Dm:�d3�����,:V(IF}4g���q}$��:�0>��r��c���/��B���z���'��P�!	�����3�_�Y�R����g�������'�}����*d�X�������}}Y��@?H]��p,�zbc_�nw��E[qD���q�����q��%P��0�);��|l#�$$"���������?d���Q�[���j2~�Q�~�j�������M�����c��@�7�����g��F����d���o��j����b��z�`;��a��{{��D���:����bkw�O4v�?o26�n����t_�o�9��������/�:���5��(��H��4�)�"�j���)	�1�B�4��!�`�6�������QCF��j[�������P6�sDBcn;��������C��a��!���CMa�����Nz!���{�y�����N^z�	������3k��G���j���e�������y�fC�f�\��R�V/7j����2r"x���
��9D����h�
��(m�U�Y^VU;�U�L��wL�X`��8A�-����hOE�r`
g'zb�g��[��������hd�a�����xf��s)N��M;��'�������t<F�.d�e��
����lH������`0C.�C����fRK.O>�l+O�#���?�*��b���\�R�@����S>��9��9��9���.�(�#n���e����I�Q0����4O�0b�V������M�,[���&�{um�!�E�2�u�wa��e�[�^�J>�tT��W��!6&�$�FoMw��j���*D������!$Rb��c������^�m-��q��Z��9+�5j�zM���2����7C�mC�U
�.�o��u@|^�U�x�n$�y	8/�>&f3�S%���w]G��F�5IZ��WhREM�j���:�,u"H�P�vS��m�
�z�_29;T��2�2�V��Y��f�f�Qn����/)^`����j�tSv:z2#�����z���w���8�TpP&����K�����e�����m��a�N]�hwB%���2�\�!\�l������Q��'
�T.S%�*���4*�I
mq��9�j&�"F�D�}V��!����fdl�*�z�Ze�%c�����%���/�!'���+�2Z�C���bD@A,����U�b�&
�1��--'���_�j�VA+J����[����GS%�)\���=E�xp��ZOF�]�]TjY�cF *����Ev:���2q�>�������\�_K$�,�.G���H�Nhb�lN
���^g��T�1�j�D7�y�����tb���r�L��U���x�D3(H��N��?n��i�&���x��xp�s����+[�S�I�c�/6���z����E�"C��C64 ��&?3�>�`&�r�f-�;��^����@C�#�Aq�	���`D&^UZ���r�M����B��q#�Q�BFX����3%�/��;!ZO��J_J������eN�MPh��~����,x�
l�U(��6�ZF������{����>��I�]P�+�M�����f��:���+�g������A�V��@���U_��
�Z�.M5�LK�A�GiJ���i����eV+��	��q��3s:yV=��>f��.���2*�cg8�n�3��H�A��
{�P�f_��d��r*!
)����&PE@�e�x�h��Q���&f�'ii�D������uFQ�y�Q����<0*[3�h��=hh��[��������J�����y�G���� H�%����-},�`��[3F�����4����0��c(��3n���� �`������s0%��z
�������/��M<2��_��)��S�-U�^��V`@B��m������4�����+Mf[���S���Be.�W{�)����g���	`cfY��Je���h�k{N7�l9��Hyo%��i�$�d`�:��T��R����H�M��S4�_?�L	D�g�&�
���N����������s��YN��c�j��[�L���>*��������������=)j\�/X�E����3��='������/��x:�A`"������g���b����z�`#����
��4�Mqi	;�w�D�#Jx3��`��K+r;s�s8�y�����A][�A�ELf�0�JLF/8�C�t�%�f��T|�a����p��B�A&g.�	&����pk������j7 �kY�ie���PDp���u,z�hx����>��Q���.�#��E�VN����e6��_I����l�G�4l�2C1�;t,�(a�����������/E����G��G��m����!�����B�V��`k���zg��-a��[�j�l6�v���m�i�o��
�j�1DHJJ���~4�1����?
�#�
��h������#Ha��'��,we�x�`�r>1�(�Pyb�bA����"��x�:Q���T��/![���$93��V��7���P��R=�ih2�5����s��7��,3�8�Dk��#�!��'X�+�4L�%F�P !+{F/v��hv��f[{'4���D�{+�t��\g���<�4��H@(S-{���w�$�:��
���������
��-�����O��%|���oib��1�{��bm�f}������s��f�j�����&d��I
g����h��������}'o y��_H��P���f+����Lf;G%�X��z�:���(�$��
4n��@e�0�K��4/dm����x���n{����3����V�g�uQ~���6���I�@��!����w��&���
v��A�~���7�l��,"���Q�9#'2���D�=���G���`�>I���'w��`������\�Q�r�L����->=2T�.{����3^�^�1����BP:�dx���2D���������o�Z�&��q�#��d<1�c�C�\��Dx�i��7�c��e��#
P7�����!��	���}[���S��-C	���u�Xn!��v-�u��b	,20�T�m�/Z�gA��G��.j��clr$$��v����97O\�A��	����huj���\�{!���[ U"���}�8k���uZ��rU#�����MGh;pU�,����{`�X���_�U=�;q,��!<��o����.?�VK�;|1I�i�~$��U� <�t���b���cY@��	b�[�zs����!�+~E��~-����T��!g
��`�.:�jl��2�������]O��=��M@7<������$�D��P\����acQ�\�4�%eH'��I\�z��(����^&yhk���B�B
�0g�������'wE�|r"��l��px?,�����
��B���(�^��ruc���C"h@���'���������Ia��Z,���@@���++�K,N�������7���q���;���/~5]�f(�I�8��N����':����a����CF������A���X�0,��G�Z��M�r��k:���4�R"�BV�T���4���[��s~�*��q����B����hX�0��gF������Y�
1q�c{L���=,���K���qaz�{p���[��	��MdA��'4�z��>(z����C�����W�������<����p!���z���M�!2�-�+�g	�n����1��9�8���&��-a0~4u}o��1�����tJ^����**l���Ar����}�<�T...j�Y}������q3Irg$����(�����D~��.��������J�y`�]#�VU��R��U�[G1�K���A�~�=&�������l���tg^^W����z0��q`z!��,RfV�
>h������)v�@5�b���X�8ha�M�,�F�����w����e�i��?(�+Xpp��J�������O���3"�CX$��znL����{����f.��'X�u �Y$P��YY��
�~���dy	RN$-� �
�7�$�4z*���"C
����+���}�_��I�H�H��)2��2'�_@��b
���3���@���\�W��$��#��!v����b��*���/M�e:|�������jJl��������u��Z�C�����dn���c�3��'���]G��"6U�H�E�m�',e�����r���Q�	\=�'�~���B|�n�k��y��qv����4��@i���w���n�?[UO+�U���w�^��U��au�r������b����p7��hw!��L����vvF��R����7����XW�+g� +��Q4������xl��O�!�Y�Za��6�����BR}��,��C�?�`G��MK�"t��|�����]<����#Nc����f�_��9f&��^���0I����E|���y����"E�'��4����"�omQ��KH�[� 
�w�{���V%�L	f��X���[��%�7)����=�OPB�	N5Y�`zv��#��N�?�����$�@��_��-��Z���F���TZu��]��Z4(;=�6N|	���{�CX=��#D53�a!�����x���$��/����v>�I���\��Z��f�$�Z����.����P�yFIM��)m��M���'q�'j~��UG�`[�O�<���D�:�[-��!��L
+G}�A�f�O%Ty�����,�['|���m!9�>��I�*7?+4N������j��!:\�	�4^1�YE8�o��?�dZ��Q4u��Z�qi�lU������/����E�=+�����U`��6J�_���9k��]����y�z��Tm@�1Sc��`@V���,\Mm'��~����(c�bP�R��B?qaE
WV�r���OM�uz���x�%.�#��1	0?!a]���)\{|�_}�L�gtS$v��V:�Y#�{�k���P����@%(}$6�H����>`&��)��r�t"=�2�2��r��B�|��U��/����T"���W�4�mz�&�����=kW*V��[�J��C��C�9�8�y�$��t�Qe�O�������&E�6���l��Xj�����-W}��(����-~��p��aq��(�m"���7W������6��|�<�g��/�B?�������0M�$��$r�3�+Zk	����A�(V�0��ujJ�rJq%t�FL���Y>4��33!R�My���B2��n���XT:_�P����{P�Bm\y�0��b ��n7�[��z����*�FB�V�� �2r��8-�eRw����c��'\�����A)�r��r�\|��!;�r���S�ETL��~��K5���W,b�
��L�y��'�v��Hr���?��8����O�rAC|"vdv��;Ab%�u��5z�>fq\���j�	t���c
�7�(\� ,��b��O�ra�������V�_T����P��2�i���&&c�R���{��4�lQ�o�ST����������<,aG�e�H8��$?~%(��
���|���=U�*
��}�{O[���^�^{
����>����������X�����F
A�6�U�t��~�A.��np�
;�z���
��@�?D��%���|����NO:����������z���'��6��x
���vAH6/P#�����%	��<���9����A����5���}���!��@<�����+(/um�������]~������4�����>���oi��5���������=h
�v���U��+��/�RN	�`�IixmU�xU��6So��
�P�����n.t��ghA�R��k�����@a��x��Nu�$\UQZ��|�� x�M��G5	���hv}w�|��H=�\���	m�9���	!�"�~� �����2L(�HS���!?��X����h�N��3����I��~���a9B�<�i�#�O���"�a`�c�&DRo��g�4S#�\����o��0�"Z���YUX�G-�>M����AI-��z-�.�t-Y���h��9�g����w�9�\Z&J&2��i4���z��d���u��B��Yg@�`���^4Q�q-s�h_j�}Q���	F���x���:�����g�m�l5���������	A�:�p
�D�����	��uu\vc�'G
>&�1�nlPl���s`�I��0�nF��@�.��k��,�A���j������&�8��V��O�E����vI��h:����S���,S6�}c<��_��Vm��lE�VuBwZ_�DVw1B�!]dl��(W�B�e����,E�ep���W�ND���>Eo��y�N�e���Y��z�/Ou��������QB�����iB������p�*���:f	����]A�y����Z��S�M6M	�G�:��yk���v��[w�}}�Fc����@F7:�p{|>$��c�]4p�O����^�������c�xd*Z��;���.�2�1�hF��1����
�`g*�seb�Wfu~��=6�I�M�U�T�i9�3a�G��e��`�
� ��2�?���P[�-rCV�d�w����#��t�����{�����`�/���,,��Jb� ��V��#����]��phz�[@����oG����w�r�1H_�sy�>Kue��|����]7Ny�0���?\�{K�izU&p�������y�` O�c�,� ��(��	1�xsTz���&`+�������4�Q�������w,���������G�����jX8�ipqW�)�D��(�%����xA�$4a��B�x��*�N""'�y6��_�^�nW4HX���B����k�{�l�{���I|+������W��p��?rs���{��
��x�,y��/}�d3Rx�DTN;0�4!21A�.�r�����]����/qO8k��e�	�Kxiy�Vp�x{�he�TzJ"!��m����F�L�$C�����[H��dQ��Qa�!��t1�:W�@��_���=dh���������AEl�@���)�^g�+|���"������t��f���#8e��H�#|K�P�]<�ZP�@|K���g���JJ�'-�J���O8y�\����.>w�=�	|4Ad��!�@�"�9G��\�s�Q���5m�"�u�dr�[���'�ln�j��>7�rA�%��5�6��@�%*/�S#)������}�OI9>fn�:�42���'}[�<�K(f�?�$��=Uy2G����n���o*��f��������5�����{�����{�.;t��zwq���uN,2m{��8�Z�
$�lV�d��1�p��l����Lx�&����A�5�h�D�����q3���Qpz�k�#�������N9J����:X���z�r��r��F�#��l��������d��O���4����/B�h.���99�?k���q���m���(����6� b��b�]������N1�	�f�8�7�_���>�|�<y��'���noB|t������������z���>=?�@�����FN��C3�~S%|�i��7P��go����h�e(������������-/��\���U��q�����������H�I~�g���1N����H?'�9���<\��TA������[���mA
��:���{��������w�����j���i)���hI��x�+P��<\Ho8���T�8�4l�2b�f�e��
m���wl�!$PG/8`��O�i�Y3�|:yS�+����#��Q4-�����%��uE�Es���;O\���8C<�W��6��YrR�t�5�����Q�bf�~�?KN��t�����;j+G�1��Z*��@-�QZJ�h��5��SZ���,L�Jd)���2��(,�����=�]�EkG�q���w>6�'O�& V5���� ����A9�-�h:�C�c���9F]�������������W�b������Km�����;�1�Q
/F�"���5�6OT�3`��Li4T�`�Q�z���}���05E���2��c��c!W��y�mG#�1�1o]&�3�z�<,�1������!r�@y�-i�GKJP����V�����Q��R�O����9u����n.L������w�!f"�Q�8���B����
4��)��1n��SD�tH��������Kz��Qz��Y�x�_lWRT�[)ZMj��6�?RrH������[y�_��.���������UI�%��vB��� G���m���W�:�SZj�|MB�m��(�bS���^�>����K~7
��&�m-��#��l��_n�����X�.��xel��+��*����2X������Q���_�����r��� ���|0{�<������������|�?C�3���>�:����6�.�i�'��,yz46����h(%����E��������%�[�K|9���	����q���
��W�Q����d-]F����C~���Nl������j�!�#<��Jo�pQ��������Z�;���OL���4hs_HJ�OM�7�\��R3��PS,qR1u<��J{��j����\���Oa������|X������->VG�!(������I
i
O:�������:N��dk�z�����0���8ZC�<yG*�^��rd&�U��XD&l��-v:��
81��7�&w,+6������+��r�����6��1t��]����tr�3E��j�����+
�f%��3�C!�h}�Mi3U�����k)���G�M)1+�����@?0��o&���
^�|�P|���	�OjSd6'����4�\�����)��OE���$���J���G8&�D�=H��U�I)�,��8&bA%�|���~��$��x{���T��F�e�'j�����W!�QxQt��?s\���SJ��
�U�9����y+��Ml�L\_�*��:�j{4�H�����d3>�'�/c5�����x+�F�i����<�
m�����
 ��sS��L����W^����F�Ax�f��a4��s�i!�&.����[~��0K����~:��C�(������R:�A�v���e�V��v��<��9.�c��%@�uO��#4��`T,K�����S�VQ%��GG���w�i�O�*�M�LTA��<t��z�N�PD�-���0}[���P$�4|�~���5}g��ud���x"ojA#enW�:��7�D�%�f�-"��D��+~4+�����y��h3E��uS^���*[��+4����33l��1�C�sg0

T�����o�zGW��`��Z>�����zm=Ej�P�f]Dc��3�6��	�	����I����@��-��J��E{.��pS�8��hl����a�N��/X����Z)���z���-��c���W0���
��V������G�(��Y0
c�HK��Y�0G�Ty�mFu,����
R}<�By������S��T���<(M�v�?Dgt>d�����J���#�0R����9��+��L8Z����Rg��9�����&���	������	���/���R3"%�2��^�0$���j0�.��e�hB\�����+�2@u���VG~_['��u����9�k�=;�����r�����x�O���y��c��>����z��.�jRM���4w�fj�Tu�*�$@%-�X;���!�S�L2sM�M�Haom��o_��>����<L�5�#��<p1����n#5�s`������g�2�8�I�pC���fZ��Ia;+N�^���]FiUfhB����{�~�.�������b�r���S�QM
0�����18���*C�)Z�����j��q����b�hb��&��QB���\?��l��JB[��2tCIEG�%���i�"����U����:��Km���M�����$��
M�����M�>�<
n�BVx��]�����:�X�]w�F�*Z��%��1
v6�R����w����V������U������z��0����SX��WVz�����~����|> �@�
O#���	��+rX+G+
��3�tmd�o�&�2M�������_VrDVE�p���
��|E��Qq�Go8���h�d8yx�/!7h0��(�S%H����zY�����_������n�������#q��|-���(�n��h�fx
��MGv�~	&G}j���`:�"B����K1�-��P�z#��,HN���w��r%�xPiZ�F��u����1�q{���`��FJ-1X�U|��0%�����}�4�RaT ~��'pe�i?W�sGS	4tI4�	��}����B����0��T� 
bo9��YL�Y������0����0
U�vEt�� �DC��[���J�������5�w��0F?����_�Y�m���,k�a�D
D �'�[����C{)������rb{}�Q�,��H"��[���J��UdQ��*�G+w���]pCn �h��e���>>��2��
��p����J��}K���&|���"�g1�q��c�����n��E?nrD$e�p������y	IN%��'��R��
����0��_G~<J��x��L�����k�#Q�
���c�5:��2��8��i�����U�sU~�Bs����*KD�w(��w��������A��{�?j�7K$�HwT�?U��s���������L�N�s+E('�����Q���bL����g"�������5c���q-S+=ynt��ZR���d�j�:]
���W��X	k���j�l���Wo�y��X����W��#�S�r;�d�Q_�-]����$e����rQB��K3p���:^���=R��yK<�S������K�|�����F����;����;�(P�a\Q������<���6�Y��}��S��d����"�7�V	��-��u2�Y(�K�:*3]#<�n�k�����;�J�F������$��r{��(����4qx��Oa�����T9���Q�q��-���M�i�9�Q�C����X�yx��g�\d�2���)O�n�+�49�)��3R�z�W�
_�
���tS�b2!���Nrd���l
��}W������>�$��w	��9��l���U�����bN��_^����!)������s��>�B��K�->��^��������
�_/��������I�\�>U��fO��!���*SSZxS�p��-��xG��2������k+�s����,�@�m�=��V��H�:&��	E�U�TL_�*SuVa��~L:�p�aR'q}���1�I�4�����:��AB�����pt:�Q3FI�������� �����~����
c���W����>wQ�`	^����H��<a�*J���,V��W��~����;��lT����>�R�! �UTH70Se���"����{�g���2L�*��:����|B?���|��=�{?����u
�����P��-Y$�'=F�]� i�m^K���W���t�d�U���s4�S�����-�|�% @������v���B%�9���\:���K9S�LC�_�Zr1ff�W�F!��3�)��j��Um��������b�H�a�)$p=�@G��P >�'Y5�����f:�y������d��/*^��/���by�WF�W����y]C���������V6�eR�Z}����7�R7�m���#�K�9vP/�p`����y����(�%/0**�������t�D�.N��f���,(����$���Gv�����_��k�$gz�u���J!NXK���a�q�h��u���E������&�]O����C��,���&�2��k��	�,�W�[G��:������ZZ�j��X\��+����Fz#���3JCj{���KmF�!�NG+;��51�)�JS���+f����N�TlE����+�OfNC!�Q���wCTLxz�;Q�=���%�>(����"�So4����KQ[�b���#�t���	�����WvF��i6���T-=P)��	���Yo��������10N�2���`�4Z!8'��@R�x]S.!_Y8N����n�c�=��6�����em8j�L�J������C�J(5�oJI�%6��S$���!%;�����6�G*�k"Ql���/_d<�8�t^!�x����7��������g��:�l����[��"m�
r��������J�3@��	F���g�*"��E����~�6��n�%�����F�I�������,������KM�.�,(OI@=�0Q=�����a���E�����:��~�4Vs9��9��1s���������j;����hS� i�g��@M
�rFwg8f�����{a���
U��b�Z�_����i��OQ�9�����lP�6\��2�J��j��D������K�&�xl��i�X�8�#�c��h��}����[w�cfA��C�q\/M�"ta����h����)v�Hd����^�N0����^��0mI���j0C�=���n.��_X|N,���}���"�\4�7U�P�����(�����?�;l�{�Zm0h��wJ��T?d>U�$��uZJ�����g��;���G�W���'�?��V�yWq�J��>q���:��A=J}���*������:��Rw���U�p�S���*�7�So}�EY*T5�8��]�~�?������KvCY�����E�u$V$P�x2_C*��&$�X��x�(���b�.�j�������M`�����O�v����7WJ���D�0�,Yej�c'Nl/C�f�Y�a��������yU�jO���������m#e�����T�A����GO�G�\,��{8���i5��E#l��3�����ohs31�4���&���OJ]@I��GC�C;���d�~��3d�����T���FD�L�*#|l����P�r�9�)�b	�on��
���'����#���G��BU�A�e���bl�������5���}�T�����{������lefs�i�r'��`:�7[����s���D���N��@�Z���`���W��W���-@sz*	lN��g��:S�<k�4��u�o6ZSN�))���3Y��J��`�c�s&�.���	�f����l�	_�H�.���*J�28�9l�j����N���mwR {��H�R?@�P���X=RD��%� 8*i���oF�HG��9Z�%�gT:�Q>ZZ��o('�;��,���7���DW���4t�v�*0V���.��t��o�n���%�-x6����C�}� �(�E���>����m�uz~���+Bq]%DwWR
�)�#��b0��N��e�^cdO�>ds;����t�,����I5�R!����e�������^yI����:3uB3�a�"�Q;L�/�-R�fb���#9�o#������2���3A�M��fO���
-��j�{>�A&P�"X�Auv�����HSg�
!�K����-~������!����6����6���^,h������O'�
2�"��>��-]�H���e�L��s��S�*��`r�>�L�~�:�E8����������n��#�c��^E��[��V�b���)�%$(@��{Vf�V�nX�~�{P��0c�t^���Z���Q���Zm7�F;��</�O�K�d<B�������J����5���$���d�������S)�:����(��-�'Q�z7	�O���|��$��vla���Y;��~B���\	���7������	�'��:��L�%~M7RY?T�A8y�o@>��<��?%�J������#��-|�4���U�U����W�\�������V�6���e�F���q����9S��>����}J<�9Uil���y��3I�x	�B����*����:��d�a�ZY6�O}�
��`�=_R���p06Q;�����=^E�Sy�G�O�Ci��r�;Pf��h��V~���B�s��&����H)au���2�� �Q���+����/;o��MJ����������<�����	�K^�y��!��P�n��c4�E�n6�U�z�J���>���6�7���I]D���P"�2�����UO��<?P.�t5��N���dK���e:�E��N6G��`��8��g5yI$i��:p�?��)��6�S�7R��(�{�HW~��0����
�4���)�O��� L�R��+]C�U}t�
���px0���Z}�*S���Hc��8���SNzB�k8�wbw��a��X<�r���l�5��V6�f���^�~)$�~�~����(���U��{�?k_���s��O$��,7����wB���A":��*�1e�C; (��O�qR5���X,��,4����T��ghMl|�yp�9��\^�^�:�������M��xg*�K�M��(�!�w{��w��4�����������Y�s���	����X�F��e$�=���y���'9s�'pC��E�<�_?hY�Q'����>m�D����U��Alb�V������G>o�<��4��Tt��`@q�g�k�Z���\&�H��tys�Qa���;��������r��'��?O�O�jc1o�]���������BJ�<����)
(E=I�
�Xy��aP�����4�x//��\�r��UE����p�l��j{���2v=v'l��F�%v���n"_�D��
��rJ�0Q�����/��������d
����~�r�o4$;����~�4���}�E��m���H��PH���������������O���5e��;�@~*K
���M��Y�r_J��~��l_��-��#VZ������&�
;P��G.f��6��~��
-����a�F�}�1c� n���:�1��{�	��h��J���������v��z�%g�K������������KP�K\��;���-����8���st��gU������jP�M���
���!����u�*y��
���r3X	b��D���NN"���c���=w7����[��S �Zp������#���/��3`�V�Q��t���{?YK����?��_[bY1�$��dk�)��(��.#�6���i&�E�N�b? V�c7B^�0��R�����bJ�����;��A�����FY	J��J�R����t{�V���/y^��1y��;�J=Z:���d
��p�Qp{q}s?�����F�<�j�^?�)2U��Q����;��q�����/	f��,#�����z���U	�[���4g��VZ��>���G�i��B�;-�y��J,3S��^F��k�:�\]��;���;����4Ccv��4�`��+��e9�(����S���d�Y��S���&Q��'6��	1���'��J?�'�:R�@8%d&���OQm��c���)ZZ��;LO3���$4]?�;���,�1P��h�b�QgG�%��c���h��Wx���H!n	}�ab��0���l��_}������<[�J��F����H���������O�3Y����9��\��� V��u|A�m)����e>E{V�����Nf2��/It�X5.n1�u�#����^B��>a!_ 
�Po@w�	����U?`�1�,
���UQk�|�PB�N(��
�]Id�Q���	�j������^XF�v�)���D�XP)���H�*����G��Qq��df���Eh���?�D3��h2�����6c������W/���/��.��W�K��Z��{-i�V�z��<�����;���4Z��:t"�����:Fs`P
��4�'�����t�f*�b_�$%��^&R�"�ZE�����T��E���]�r�N�Pl���}����]�2������_N��&�t��������S�piT���c����O�;�|�/�T�G
��"���������_��)==���`!/���{4w�w�*�E��Sr�i_~�E�6�����l��O��o�t	������)�/?�`����p�\���~:$�����p���U|��������I��:l��y�s+��-�Gi�9�Gz^����}���i��z"�<�q�:o/����U����\���2R+���N�P����6��t>W�2q�O���6��	����m��s��B��Uy<���#���[o�{�v�Y�7�{L.��9Ax�J�T����4�Uw�_��yw��Z�w��Y��hU���������;r��*	�����������������������A��w;�������y}�������)�����
l�R����K��p�;�0����:.�+t)���t=��j�;ac�S&S��G���2�/������*����k	�	(+����U��$��5�/�GU��U@��t�%���.�'�!{t�'�[�w���?�� �/�{���stv���;�u�8|/��VV-�A���N���������o��T���g��{Sv��?����b��(�*#����@�T�f�%9��9���+��V��$�o&��m�
��6m{�7S:���~�*�K���&�3���%�
ON�����7o.;oVK���t}�r������%���{'�p_�Tp4�<�\O���6�	��N��o����>A�R�a�@i��
���9	����56������N�o��n3�Ye#�����ff�d���#c�!�,�pL�GkV���'����9���a�s
�F����f,g&s��;U�.�f��6��8�=o���ZV���VS.��6w&�;��t��u��<�!�GS;jc�(����/���UL�*��	E@��u<{
7���!M�~�����nj�d��Y�Ol�p��
������j�e��9�?�m�Z��e��?�}h����;M�W�?0o������>�[v��@�������H�UG%Z�a��PF�M�g'	X���)�����A��'���x F����CV�z��������O1����S2��K�������fA����o�C�^b+������)I��������wG�W	�H�������� %��9E��R�b0���1P�Q�c`N*j"����^�i���z>6��������H�U������P�2����Kl�Q5����W-�FS�[��?����.���;���I���J0��p�>�suS
>�FH��|��j��@�^��� f`���t��Sc+O��8
=�|�Sm����\��q�/�T�[�H`6�|��~�_�?K3�N��S��'���Bl5c�c��Z�d*��PTMr9��kY�d��-�����{u���q��)�c����=��1?���gG���l�@m��dx�qt��^�Re�wp��=8��vZ�A��_B�,���R�
X�u��G���B���Ki5Su���xV\��C�9�,�h��;*N��}�N���lS����}4=�\������"W��Hw������4h�c�i1�Y�T��N`�@�h[�m��+M<]�R�{�������������h��R��&z��@�B8D_A�k��������%���Y�"(�EV�5�PCU�'���������o>�r�������I	4?�Y���>O����RG��z�T��t�#����Q��������������fY�����_���'��"�3+
����t��Yo:�������)�M���6�_�+-\p������+��+�N:d84n��A*%^��y�V���-�<��;�bg�-N���������x9k�������t�B/��8��S�	Ri���jq�j�D1��L�F�%�E���%��?��W"��d�$����d�=������.�E���:��7Q����rL���� ��pk������.��Z�uU7@�����������)WLb�z	��>-(e��+��	�mX�6��VA��lYa)[��v[�D�[t�}�#.p�J<T'O��<��{[Ff�D�b�$5n�P��2sb�x�f�m��+�[���F�l�j����������5��svb���������)�IV�q��}�[���9��N��������m�.�(T�nh6_v1S������GO�g#3�rvRhD�F"�i��F,�;�G�~U��2;������w���&S�b���|R��p��I��d�[�-��P���E�6��IN�M4����h�.�@t�=�i��� �U[��/�D|@�����E��L����S�;G��2�o�>���
��������,)��x>�-q�F?k���}w�C
7�G�d��2��{:!q���^�">�Q��.>{�L"KM#F�
�=�-epD�l,��H�����i�;�n��5$�;���U(��t<�"�{�}��-*�C���$6��dk����/G�m���)��Q�	_��������_�!�k`>NJp��k(y�z��4�z���X�e��t���0C�}=f9HQ�)��7����������v�>���'b�={$+D��'�	�rv���V�����~�`�0�t�'�9�H������E�}�TJ����.�������'B=��t���k%�t�^��g8OJ��"AMj�j�:)v����q1i�����������`�\�������2����.)x9P���w;���^y�(������K�My+sh�J9)�V��z<�#���������#�f�G������&Bo����~J(=>�5���>W�f/Z���9��=�ci������`�����9�ba�����p�,��f�pk�:�c�V~Horz���������I4�|������J2
�G�!����-�D�O�����9�6��#?�I�gg*�y���=�h�ee����s�����m
���x�}�t0'��$������~����Q��WA��1�v�J�@��1*�
S�x+N:g�^�#��n}f���`���
�hK8�X���
�[�����Q[b������;�:
��kn���k���(�G�������/-�L���,����@���9~{���s����x6��Z=k��+;��2F#���lE������n��+*l��D/�(�^}�V��y���#L�-�o����:������"������e���_�s���
���6R*�G%UV����Vm��r��0�)����8��r�i��Sj��J��V�	�rZH��:%����@�"��9�Y��*�Gl�R�+��<h����622sJ�}�+����!��n�;�]\�~}z�y��*�m�
:S���v�)
Vb�(�����+�����94�St�:���zZ��������H���t/��Lo�9�� ��\x��P��P\�\
nAfg��b�X������K	����Co�j�N��Z�j���D��7��E�I���*i�����vY�%�j���YM��d�0�g�d
���$�fG*D��w�"q��N�Zr�r�\b�`t���)�����������x�s�e��������M�W���ep��N��;�=�������W.�|�Y���d?��&���.��|)#'Yhf�U/��x�����9}!��5V1��o;>A�'[�+����)��Q$G,)�!�2%j~���)�����{�j��@x����*�-�9�������1���I[!��yO�\L��=DHIve4h[W�k��x�.����p�m�7����0(��]
P1���c6T.����L��:�SL�T���B1��w{���5�s�d�9<���<f4?�(3��~��g���ee���ae����0����2r����J�K6`�j����Xs��c�O�V����q�vr���$�6ax�r�J�����.���������`�N�y�+�����wo�S==���MS�������;o��**U���C�Woy�1�IZ��{��Z�1��$bvz%n8V����z:��<�^d��w�
z��`[���a����}�fM~�������6���<�8����mL.2�"Ox��sT�W��I;^|������9J�������q����j����22�[�����_���t��&���E��z)����E�_R9�Z K-��t^�-I�+�[N:�g���������_;"�7��S�����U�e�;���RJ�����&�`��Yt�p���?'{�c�s���nr�r��R�B��={��h��158o��������7@���,���hWYqL��u��;9]f����]�/�o;��e������zNC7�\U�n�?�����p��p�:�'w���m����i��:���V����k!y,�����7�]g��e�~
T����*���8�7+��6��N��Wn��������[V�O�����&}MC��
���]�;�0w�a$�Y���G�]�5��X�M���`���1�a���a�����c	�M��e���e��cb�`�����5��zI�T��@��#4e(���,�{�n������V/�a�d{���C���]��
2�<������A�K�M�D�&gw�^�rZ)3Dc���zy��)�������,f�X2��y�)����?���{A�����^�z(snZK�����^��[�1���+��}��d���W�T�V?Z��z=F9g-�R��^�R�Y�kE^�3I��2���H��D�������d�Aezp� N7����,�����N�l_������������fzL]�WCd��gf���'�����}����Xg��Zf�����T�#�,�+U��X���e����������o_��)8���;��Y���c����aM�����fm��>bw���=��z�N�+���a���_r���Q��y*X���p}�h��Z����7��_��`����`��|�-�����>?���{���"��/�4�~{�
b������]/;���Y��}v����V�y*�t!����0SOnU<�����.N��d��R]�/���Z
��c��=p��^W����R�FI�9��w*��k����s2�rJZ��9#U�sB�IGz�-:�5�
:��q�[��������`����S��`��p�A�������vo�_��U���.�V�x���x�S�+����Vw���^�������d�9c�G%����WB<fN7����d6X�,YJ�0������$��2o3���j���Mqu�C�:�WHS	lRT
��.Za����EU(�q.qv��S;�k^���z��"�M���r4�M���������p�����!+Ur���}4\Q�1H��:p�3
��"+&�O�wtB�\PP5
`a�K1Q/�~=�!��x�U�aY,P���l
#k"7�knH�G��H,v
��K@�B�ZG�������+
���j�
'��/�	�I��:FU
�)��Q��	�6���
��-���z�3b�����c����5.f��C!��<G������~A�>�/���X��&Z��d�2�����������&&����[t}�B���q���P;�9����sb��
�dx;�����A�N�����t�o��ee����>��^�6��OTAW��h~�_v��������?�������o���|3�������N�}��lyK�@����~%yLp����;?��]������_xY���IX��:+�
�K�8��������F�Q+m�k��_hf�e����b1�Y�,[jI��.\n�x����
���[e��Ul!�A_,�r+��
Fb������cR���D�5��VY��j{�r��X�k���?*���	��|��:'��8}��sr���rv�D7U�`�h<�����Ka9�G���$�w4,Q;{�}��I�
Jq�gD#ze��@I�W����|������/O*eN�Aq�w���R�,W�(�,wxRm=�Q�(h1�1Y���d���1�j��n����%���v����&�K��-R|�]�e��y���v�����<��0w���n�����v�AQ(��*��kE�bV����u�$����7���v������5�����l
9_�B�57���6�MW��$��9��U�������B���8�V
U�]~sz~�������rU����f��R�f�LT��`�zw�Y�
w���A#_�������r�-�p;-'-=4��S������	�`&}��i�����1��>W����J�I��;`S�~BJ��L�T<6���crAl�~��t�����~^�M�<�_E��wM����V�g��|����nv�d~h�e�Y�7(a
��;l���'��^�w������%��j�Yk���B���P�s{P;�XIX[������\�i]���rue�l�?3�w�3��6�����R��f���5��\���E'q2�C�{��B���	�H=���m�EC�V����9wd�����`F����T�+�^�v��H��9V�$'���j|[��;���������fAL�g�o���X������!�
�a\&�(]�(�N���P���K���,&jaN������;������_lH��:�%���N"��������=�Il)��6�|�-%0���/�?,����L
��T�>X��;���}�@f��H<���<��V�-n����%X�c�;nOA���m��>L5���������	�su�>���������g�7������]/���}LI�_�W�����tJ��]�~3���Y���-aA�
�>�\�8��3����bE��q�V�^��=}�V�����(d%���Uw����u��w�m��:�_��c���u��W��9�=�*����]����q����T��.��7W_24l.e2�'��?P�w��[���
�g���) ��)7`�KJR�G���}�������;K0N��N����U��|��;
�i���2.�?d�)rW������;�GE�3-��3g�����g�����Y��H���JV��|m&�����n�����{��>���~���O�����66��{������� �pF������j�~�W�lW����WI�"�
uTk��:m���j��6T��g�9!\S�!�.��M<z�]�'���M�_��w���e��s���h/��i�i�t�))��rUzrz���A-'��P�������w
`������+������*�~?T�����,O�������5(�e��o�����!Jj���+�����|eW�������]��I !���R��_���U0������4g����������+&@�y\�Tp��A27��M�O���P2|�+#��#�nB���ci:SY��$Q�eQ� �,�*Q���__#���4��Y��i�dq3���:��������ic$������f�M�9+����6��l�B���^�L�	�~���Ln�vm����s���#o��;,���Sa���BoS�t�>���R���r��Re�n�9?�����ro8�;*�x�\u����7F�yb��XX�m�~����������������K_KU�m�E�,��"�	D�p}�i_\�/�:����Jn�}����/�-���*�e�����6��GU�D�5��O����o/���G(�����*M���mF*�Y:�CLY^����
�r�����r�M��E�-�w�{���gh������b=��|�>��������QW��������N����O�+��r������vsy�td����h���,e����+���T%�_M��{�]���TQ���S���}qaS�L%U����_w.�������c����}������<�?d
Y��[\��u������p�o�m�)��^���������g>�f~��	A�������S�����wU�n��*���.j���{����!t�Wn��4�srE��U�������[����P�d��3p��.6���H$O�zW�]i���A�����dGZwS�FX��?J�"v]�1�F��Q{���@D����(2p9���y��w��eU�����!?�M�E�������/3F>j���:�(��D���t��Bp��8;=n[�.}��g�����/[V���+�p'�������_��@�L�����L�<���U��a}�Z�����MI�%@�K����������^70��y�0�*m�������6IA�)��pN_{l��}�:������L�����K+����dy�c?����6;�63���uj/�?�F�"�\n���k��t�b�����!�����Z���>����F�p����7^K5�Z��*�6;`�#�g����x	�)����`�n7����d�W8�0;X��+�
��v��Z����Q���+���.�uP=�[�4�n�l��%~����7�Aj��}�<$�ppy[9�l�|9�s���7�$A�)|Y@���t��Sls���Y�"�d�c��!�@_.�l�����V�Z�����p�\F��;?���bE)o�+y�IZ� ���jqk=V�{F�u������/h��������V��D�j�����Ow��/������f8���b&��&�HT�����B+�KAh.�(��.t�he
rK�Uf+(�L��'f����T`���|���J�8Un�1e{�2-iQ(�U�h��*������
�]��)��}���1{�9�NX���?nY�xe���
���wX���4�$��)cc�����w|m��Tyj}���oY�����K}��F��
�W.����B=�C&����,����[(A�Nb�p��S�8nE�$�����,r�B�X��5�=��:������*�EA��*��"Xu���p)^"}
���h��M������+b���)����=yPv��"�dO�
���n@Qb��
�!��U�g~���&�x��O�O�O�O�O�O�O_������o���f����f���s8X�C�t2~x�!e�@4%�v������	v�e4���}LP0�N�/L��`�A�J�`����d��9�pD�Ftc5�����a��<j
?��<.��<.X����8��$�]s���nF�y�8X�bx�qPH�)���n�^�[;�j�P<�&�p>��T`iR��'Q�5����a�t��CB�W2�o(����+|���(j�}�l��H���{E�,��w;?�O��y���U�����$���g7��u8& ��P)@xX��E���	P��42C���C��x���� DkE�����&~�p_��H�hy��8� Q�r��W
�i���@������+[��.���'��2��'�����\����!�����ut����B>������`���<
��JX`r�
t�)�C��c��]Po�(�'��@��N���<Ajn����A��'������Bgi@����j��6��Czn��3t,J����������k�x�XN)�)s��9��P�o	N����9�9����?ht �G$�c�M��Q5��=��Q.�:�^�J|��
�K��4�Dz��@�WE��!��:���85�v�*V�Q���:0m�.WA@������NK`:A.��HI8~H�d��l6�
��&��F����A@������_���:��j�62���!�e$����pp[W���-��}�S�����w��p����x+���-��4x/�a�s���l�W\������o�- u0TK����Q��T�x!�����{P������?�vO:+�=��+��>Q���k��D�� ���[8�i�#�\�z��c`]�i�R�~�|B�p�k�<>5l�i�5R����32���X)�e9!���)�e�����44ddiW�(�^���b��i���
�H�p��T��d(N*+�#Kw�o�P����1�h��JB"� iB�5ce��P��C�L���
�>�Ti�����N�]�i�Tw����M�,V�-V��>F��Q4�(��@��B�.��mH
����{���C��
���\'��#��;�U�+I����W��Kb��d���)��2
1��k�B V<�~���l��77���$����HAh��0u�<���8R���YlB���-�x;��;e�����Z*f�����l�H����Es����>���lXz
�4���Qk���K5*?�+vJY�W�k���m���3�#�a4��X���X�r�y�jz��l���Ek)�t��8�@�X�����k_za�ym�%+[��S�e�2�bZ
��yR�4�z�����	���]n]�!�z����,e���RR�p�LcK��-*/q|\j���d�q'>�s��`��-em�;H"
=bb8x.��|����F��i�j�C$������d����O"��# :��f��=����������A���*��A8!��eb��B�>>�j��2���&�u?��o*�`�a�>&��|�Q���y�!�zx��@���a<��Io:;��1#�as�b����wu�@uv���H����Eb�ud��U(���2s*�����Vmi��Z������F��[�����9�0>=B'cE4w&���r�%,�C��!u�8	Y�+���RJU�_}�E�����F��8���4�U�o:n��v\�����r��	d�8��ZSe�[a�`���H?5k�����_K���Dcd]�
	�Mh�WtRC���X�o"�g&�gj��MX��sd���c���
�����O�)H4(�� ~��[��_|��=��&��.��:C�}��L�7�0W��B���Z��m��$�B���)]��jE_+��]���O�U�"� v�;�z��AXc?��K��rL/�a���}y~z�&!)!�<''g��E��H[lX�$>K[��������$��d������{>]PJ&7/�.��T\��W�ye�	�
������[c�^�6H&G�.��lzC���C���Y�b��a��PJ~����c�������(/����7�C���+ �x�r=���i�x�68�+����#�h����;h�j���Ak?��G���uK��#�t��n�{�����D�_S>Q��
���er������/���r�� ����w�oK
�G��y��LJ��>�B�#<���r�Y���!����f��^��_z����C1��:��b��W��8��a�9P�h��-~6����K;�����{�x{*���/��m�?��f�2����}lni����?���#����ypB���o����P��J��l�6�w�dl6T�������x�bv�.�f/�pYRi�!���It`�ezWT������j�����A����0�������;��N_I���q�����O�D&3� F��������?�^?2I��8�\ugt��+�VP?<�{��.`�"����	r���.��}EO���2�����h��$&���~9	�$�������y���)g���o�4�����Ai���?"�����1`����)��	��<�l
�v.g�p>��}�����r;�R%�gY��-X����!
���'B*Q�S<{6�fo������ Go��@�}�S����+[��\�b���+�����
�i��oE�[�������N�"I��w%�Q�B�&L�����#m�9�|9Z 8c4z+�N�HT����A)�|�T��ETp4��u&����Eh�"x�o��g5���r6`B �_?<�������]4��L����}�P���	���l\�v��N��!H"�?�S���O8�S��>7�O���D���R<���s�J/�<����������f4Hy����q�C%��,N0�l��8,+Z�G���Ak[����J�l-��#'e�=��"�A�x�[����d:��%Q4d���e��������%Zy�����@���zU5��U�d;VUC�^��,���tr��Xq��q���,�\��D QJ;�0mLwW�Ij���:�2���O*�������l7�@P
������IW�#T�����<�g���� ��u��^��qLs���i�La�
��%�z^���a�r�3A��g�����Od}MI�]�P�.�A��1�#��k�h�l��&��/���^P�Z�g������>���\��77�SPyCm������x,�5�w��
DLm��8��@�gmR�~V��|D�1������~E��KwF�F��:B��8���1���;{��z�M����7~=u����7s��=�����[$X�����J39���o���z�Dd FW��]rSAi������z��-�~�I*#s��'���8CU/���BGx]I�S��W!���TR�]<�++2K1����k~��4�i^��2�u���#���=���d)�����{4�G��Y�
"gYb��x/2�/�q7�����3�s�Wa�0���3\�L�F��5�0�����C�e6D���!�6��zi��������������&����*���j���k~�?��;*&�UX4�J"
H���G�i{���%mi����wK�/
 l�9|$���!�E�,a���
����|�����+(���N�^�L?��%�_ClB��9�I��H 	����i
j~~��S%���D,���r-
���NP��uC}�"�o}���7�
��T�.�>���%;#��2����c�����X�h���=#���	�\(��U�2���	G*#�85��U ���	��d���m�_D���,D=�tq���Y��!�9G����	l�s�9��g/��Yl���*}k<Kz�����F�N��
L����6?��yP�	4m5
TW���^��X�f]d�����Q��W��7��=�W��c�C��Ad_)�@r��Q�-����7������"��Q��W��p�|w��Ew� m����)|{a���0�\���1�<��rK��DhQ��k���_U�	���������td	���cWW�H\G7h�6U)"e���ucT �0k]Pe��jF�Nj��-����������3���
)bI+R��_�d
�hv'5I�%t�Zw6�)�'�������R��1��N�O*w{Q��JI���������k��4pOF�������Ld�Y�)�$�t�QT_?�(���>e_�iV�����rg���������U���D��%����
����5&W�'���eFq4BrV4-��r�\�
���l!�����C<M�a��Wz��=��E�,��d9���pd���-��)�m�|�,l 
����@Z1`����3���1�D�v�}���k*��G��Y7���X4�?��G�x�f��l�?#f��*1��q�N�x{[u���~ZFxt���d`d
RY�Sf�YsY�w�5�������������U���_����6�Ob�QTj����<�����^O��u`I�3�<,n�����|� �������)�7��m�
�'�����p�;��b*�!8�[�(���HC�s��j�=e��\�������
?2|�����?2��^����41�3Y~�������zF��J<4�`;t�����&g]�"6O���#EO��A~*�':����E���S^������f,��M�Xh���>r�C�>���G�9����������x�B*nR�Q\���4���rj,�Fs+V	II���)��OE��]�x���p���>�!��e��(��a�H':�i�������oD�j�)�L~'�g'�v�����1t6z����s�������;�O
���T)7���vsZ�VJ|�KLH|B�q��Q��r��������pG�������(�`5g-W^���2�k����h�9�EL������l����S���^����p>���h���`ZH���t�!�tc4���H}�����i��1��l]�����#�Gw�����D��z�����-�e��gX��I4b�������&J����8�w�9s�Q�k��M����	��|�������(�����ML����?W@����TU�,i�kF��3���#S�-(�r"$Y#����y}+���>��d
�dz��/��N������7@z�'Mu�a�G��A�DM4s�2�*��iH�4"���j��0 �hc&&ZU��������$�~���6�N:������N���Zw
s,O�< �j�y�>��LC�;�[�A���0W�#�B��;2e����*���RH����$��I����.a�yu��/A,�����f�R�B��!e����7�p���B\o~��c���������
�7\�*����[��/��LG����
���q��%F�j���-���[T*�T���$��� �>�-�t�P�$N���x�&�ZR�|�	�#������=����(6�����!�.�#
~I�,'Q�W�m�������n"#T��i��0����=
�5����a��t�$�!����{7E�9P��
�S�.��>vH�CX�v ����02d�i��=��6V�g�~ I�:���`����P����
���n�	����^+�� ����#�����[�^���|��!Tj����\��T�E����,)����ZP���0DX�`��S`���u�d�G��p�r=�nCK�%=cr�C�Z���-�[����b���'p�sY���s0t+%c�7�!Vy�bW�m�������\�pI]cR�����)0X��lq��V��f��|����[8L����v�PMa��	s�g�����h�
��
&������?�
V��RC@�b�'Un������D�����~�������haD1��,���	h���!�U�B2GU�t�DYP`��������)�g����U�����&��5T�'�?�"��m<F�t���]Yb%@d�"D�,���$��v�k������h�7I�����~^��]V���F�G���#�9�P�&��dR�3���?Z���/>\G���\t��rK1�r���"KU���d��#P�8�	�AZw�L"���`M�D�}����,h�7�8���_
�����,L{��9U����U?O����2�T���T���I��������8����+�����w.�]��TN��Tg#��I��08oe�tHBE�������p3h��[g��?Nh�^A'���`4���3��%�zaW��x�����L}�Q��?�F��;��/��.�w�����t��0����o������s��'��O���bz7yb	�p~����:2�>��G"����"����N��HyA��K�Qk�4�{��c$��a�J$3�]��]-�������[u���fS�r���-8����)����������^:P��D~�#����A��\���b9;����H��@���AD��m�2��A�$������
!#�T��-Ac\0T������AT+0f��MS�
��5������7��V�o�c���7�L�qW��DG���	��Z�����S�����r��"��4V��k+p�r�(/�\J_
��\<���ZO��}�7���H30������5��O%K+�r��_��9z���t�N�pf�&��y�5�7�$����M����tB��� *DF���@�Y`�~k��$�:�����"��>gNu�����L���M5he��.������`o'Z��505�c�T�������'���7N�p
EU~��?Z�=������"�"��z��VV����b��*��RN�]�o+��#�7����P��Le#��Q���l��9�f��@F)�����<����B��P/�dt�k�b����5�����h��A4hc3�k;^�#���TQ	%g��M|�r�"����Y����,�b���D��<j���0m�:���
�H1��5�W.<�$���E��G!W���G�����T��@�8Q�R{�BT���tbE ����q�o�t�-l���
?�/��3���J�}6��=4�w�������������8����V*#bg�`�$&����0}�v�	�j)�+%|�|�f�s�,����-Cr6������O���$I\����c}o}���^lv��Yf��	�J���ycU��B��A��D�#��:��x�_��r�����6L��m���Z���`���	\��`�-mY�����\]��pI�����xW���2ud��7G�`�[�%9��������_��� (�
A�71���Lkb�g��w��U�zn=���0�z�
Y�^4�����5C�2:�B���Rqj�Tg��v�Z mj
bQ�Wh�[����Mj0��bj�����c��-�����r��_�0h��Z'G��"Ws8J��G��.����9*��O=^UJ�y4r�2�����Y�bw�Rj���7�e�+m�O_�EYmo�#`����L��������7S��A�	����gm:C�JFQ�v���x��p����`9^�3��
1����������h������yhH��xp3�a U��s=��@5��Z9g�G4�=wCRI��������c����)_g��'����.����Ql^�/���%>W�k�lo�;"� ;\E�������v�r�������_���%�U3�[����Z+8�	��[,YX�X	V���u�^�������Y����`;��	��[pw6_�^�|���c<z�:��h0d9���sknp�3���W-[�;�uk���/\Gq�����Y�i����v�;{�K�l�k0*����Y�!g���u�d��x������}�V����|u�vr��:�U���T�����(��	9�mo�9��:=����,*����x\��)�H���!����X��rp����Tf�bh��`c�Z����3�R�h8�j�X�*����o��IN(s<zyT��^%���������'�oQ��[C��l�� ���.�`�<9�"����D?���$D�����m\���P�����0���r�b��.�"E�[�O�U;E����#;�xy����"���b��4_	UW��J�B��_���y)�	�W�@��y�6�ij&*w�1c��d,��%�����^�v�-���Q�������bb����fIQ�)�i����J�u��	:��3��g���q�Q��1WF=_�������4��wv~0��6pM�������	T��D���5/��S�������<M�����?	���1���ZE!��) �0h�!�v���%��C/9cD2��2�A[��Ik/����e�F�U���C��/�;jGLP|�e���+q2�o�E���0���o�����O����;(4��C�v{������ $�e����z6������,����5@��)(�������.���K!=��=�C}(~��A+U�'�(r�S��D,nT��I-%r��
���H��Re�(���sW������:[���F~[������K�G�Pu�b�X��Q�&*���q���y=sW����2@f����>��W���}��'��a��>��cv��gcC� �u�P�g�����P��T�8�dj��;�
�Z�F��ZW�P�������
h��O��LO�>W����Uh����e�����e���5���gj�7P����R�!���H���`�	��:M�1fg����������9��p)u�sn��`lM*����A`���S���q��n�Y�f{���j�i�R3�#�@ f[\3#���x�-���1�tVe����O{�L!f������&��!�=i0L8w��Q�o
��Z���m���'����?��)����d�w?�Y�3J�A�.�D�N�r��`E�����������8�����Q�Jr�����/�CJoU?l���::8n�	�V������Y`	_c1�^�>���Pf����[����on�����<c�(3��.�T>KX�z;�V��.�V]"+^�O/��	�����h��j/�>�}s�������<l������>�`�CH��A��2�Et;��n���&��n�N�xShk��(�\�<����I���z���Te�:.���<{���''dD:���L	��o�Q���0���K��o�r���C}Y����V��Q�������o��r�h��-��A��}!��3��&!�Y�H"��5]�
"y#7��e���,��)w�E�5��]�4v���%-{/i�������Y��l�C~���j�}	��k�p9�l���qzz}�}8�8��{z�1la0C(��'Vs�����{y��^}�S�i��9	^�;?G��"��i��Wv����������e���{wq�t�);KG\|�a��yd�W�������(+u
,k!�_?y�Uu\����~����|88U$�2��ZV9:O%)�*.���SK���nx�J�U}�?�Qj

f���P�AF]K����\t|/'R���Iey���� ���������,����L���}������H��$���B�=;Q)�������X�����������Q�E�rWa�0!Rg��>_
��"��0}��`���p.����|:�<��c^~&��stO({����,�;��d������o���(� �w-�\��<��fJ	>q�L[>���eX}�����K�ih	��c��Z�8tJX�i���w�>=��9�(��S�%�$��<��$�pQ����m\C,�>=P2���O*I����8���������x`�2W�-Ge}��N�0�T�;�j1�
Z���1L���,��+�!�n������������I�U�u���U$5���L"������zU�T�Q��Xw�'��jA��E����}9�^�jl��/���s`��c]F��:�L�_v�z���rT��l���]|�)��M;;xv��MU��M�.�i���
:���,���wN�]v�����Ga6;[�q**��{���������
���y���0���]^\�5���B���q,��G6MUV�UN0$5�j����^
���t�=�_t�z��v���:�
���y���������Y0��;|����.�<�5����[J-�����@������(9!E�/���-P(���r�Q?���j�h0����SW��PW�2k���r�9@���m��.%��A��pj��1�5}yu�}e�)��|����3������o-��O�Yxa���j
pK���C����w�vPi�����=m�faD�(���Jeq3� X0�%;PJ�t��N}��i$
���`w�3�\�����!���`t.����#e�)a���@�^9�Mj��
��b7K�1�9b����4V�*%�C��x{4g���N�l�����S*����Sa�-�w��>SmMv��N����ss���H����!����QR=|�C�$d2�E���AJ����9	:p�Li~�����E�K���P�>���H�5{���`gX�^L�BZ��($8��!��;'<�J�.L�CC.��8��w?������:�\��'��`��-G
��z��h?�h8��r�������RM�h���Dw�+?$r{���It�������7�yKo4��,Z\�p����KFS��+d��W���%�y|�.��2ecq�1��la!xh�sA���b�����UGj�V@�a{u�A����L��x1���dJZ��������mg�W�^��fnK���m-�@3������k�$(�i�B����@H��7�p�V�����^c��)R�����DL�����A_�8v<��e����h�CD@���8����In��:�	C�����~:x�0�����U��k��9Wrc0�
L���)�<����/�����VU�Az�cXl-o�|��[�����&��:C���;��y��so��r��������$)�W���!YC�M���(����a�<l�{�l���U���8���t�����>Z�iZ����.����}V�~8�i��?�$�Q�l|�>����*�0����O"O����-������v'��6�����b�4YY`���$B���%U22�M�1�xa����<�z�d1]h���MQ��D5��"Tz���Y�j�����@X)�(b���S���gO�����~�j�oU�SU�(��&���PA�L��[����
��
������*��:�������iV�D���Z���u�����3�U^�������k�?�N1p5=�����@��v�D���')��bw\Z!�^W�@#+q]
������G/��oq��A�2p����7���������#[F���
$�:�R��w>�d��y�����M�����b�e��%�xAAJ��M����/)�������[d��OWPy�>�����[=��=��L�H���n��{<��c@"�����������W�Rq�}�2L�6�)}T����Z��s=�9�0��B&����=dq�������9r���]?��!�C�F��t��DI}����1f�_�=�f��?[b����m4�e��*��F;Q���vZ�����S�v��6<d��C1���;�)h+`[�|���9^�Fr=��!n��������[�m��'
�T�>L����w�45�������~w�w���=��{� �:E��N�����da���vn/U��_��	
h��]9�v{9�]�N�[�~��[D(q�Oq�~[�3���8|A��|8d�����e�j#h�VTEQ��h�`�?�a�~����N"V#������=�
[���M~a?��g�������e�m�����M��. �`���0a��k�����*������W�s~��z]�d�%U�T4ZS�����������
�}w�^u��c���:/�J�
&��;HSA��Z;������E��hu��V%��5I����^��K�������w����I�_�%������h�|���vc�^E���	��[�!:4�^�^���/��oy:KK���m���n��N�u��-ots������R�i�����XPbO��,�%L���H��H5Y�Q��!��z�8d�Y[�mVL3�s�����"'�=�D��(��F�������J�~������/��{�W����y2�Yn�Xe�ho�~�Q<��>�Dk�H��\�v+	z��a��?����� ��F�T7�(�����/��c4AB}���~����n��/����;�)��Jw��hr�>NN��!��"W�C�d���m2+�_��j1�d������s2�&��	�7XE8M|E��3�Y%�,�{x����8�>S��}W���K��*�
`�w�6��fm��A�s������]��It��,"���L\��?`1;{��p�����x-�0g<���@Xu��)����Z/=*�NXIK$�	��5�*sS��\Z�%=qOE�~���1z��������u���Q�;�����EPlpY�{���c��&�7��:������I�!Og���f>]�V��[
.�����n�����w��8M��8���98g�8B���R����v7EeL}���&An*���|5�,�m�W�����S]e���Z��'�H�4Y3���m���>O4C�83�o0r1�x����`6�?w��(�I�L�5l���NWt"y��p�K�Q��{8�c������X*�k�}����,�u��S�:'�Y5�P� ����Y���7�g�j�� �\p*3��#��V$����/Hi�Biw����$5�QLN�����"|s�3��9"����G�����y����Q!D`�~J����lJ��0������B�F���V(4���"F�"�P��}�,;��a`�xL��`F#q�^^�1k�;0�C�Ti����'�ol�W�D���!TB���z��PowU=�AZ��X���0��"���1ss����^c~�9&\��F	��� ��"R_3���8j6sT��������.L������m��,��m&����Pw�;gI�2���	5��dCL
~#�
J,����K4�1���������k%W�kH�S1c�����%�[K���p8�����#j����]XO�
V<}J\���l&RZtS����I����S�M�Q�u����p]h�K�R=�sn]��T�(��?���A��_��.��w�m�)FY\~���8M62����-i�O�A>�o@s�
�M�!�dF��b���
2@�t~�V�w����P�pz�MOM7��#�/�*b,�:J�[�ho���pe(�V/�0h�c�k�����X��s��u/	�
/�k�[=W���	9��T����XU�F�E�(�#����KsN�� 4X���8��h~#�U���ar+���������].)����W���t'���F�o���{ik�w��z.�'�`Jt"��i	�~`\����[�w�����&��c,��y�+E�1P�)���`��R�OM��fa�F��:�^�<�D�F���/4�X|J��pp���y����u��������A����>j�K
��������Ha�R�<*}��@��AG�Z�&K�O��Sd���Dh�|�X��B�/5[�7G��Do^x���@L�]�tx$�H����L�
ntjWLI����8��\9�����v�Z�"E���n�[����"�yF��
`���U�J��0��)�Y�/��-�k����������O�hL���p��X������#-���'���L��������_�~���mD���������m#�a����FJ�Sa�#Vf�X�]��M�?}��L{�����%`�V3V�����9��GDSE<|�3Jw~
�A���RXY������'k�Dk�����~�zL���W(��UV�PR0t�n5%N����Z���[EEbk^GE"G�*����!�-�B�w'-��\�7�L��dW���=wP���m�?U��i|��;�l?�q5(s�!L�t��OA���/{NHv�N�%Z�I_�<�������3L�:���>F����>:l��Q��u�q�+raj�f��~���a��@���h����tc��T��_
?���u�{�������h����~8�x
�-�����������e�W�y����3��{����g%`-���8�r�F�v��6�2tP�P_�����&(��|�y}�ykS<��S����m0!�	"�d4I5����S=���]M����r����F ����z[��7��1.{��T��9y��������2�|=���8��
|�~��y�Ig��1}TS�i�Q�L�yqu/��#�/(H�>n��aV
"1�N���'��Z��&�	YC�����5.��,�HA�R�(��B��y+,��'��D���q���PU9������n7��.JP���Bl��p8<�:��L�������U��LZ�z�����>m�b�'���6�p�(���D��x�x+�
��������S)�CtO�����
0��J��mrDm��%�	�0�/�,ey<6��(���9}qang��/8�l!:��.��?
��-�����T{�"E.�4R'��F��l����,KYT��PS:"�!��:�?<� ��_�02}�s jL'z+�1��rN�4��ae���F������uc�;�h��t�����G�����fc�6�/�Z�-��������LGz�PP�U7p�u����/�;�5�X�p|�>��~��qt��<����k��P�*VG�r;F�fs�V�����70���;�UK=��5��*�z�^t��'�[��zSc�����q������M��R��I������������=
�W�+/�sG������<*�����k(�Ig�������t����w~�t�^���� �Rz���4��{�����+���N{���������S!K�w�[�o�I�>L�m#�u�S��J.D�#�8]��E��0-��BF�L����a�`�Vk6Z{��N>�������s`��������E�������,�3��O�����g��>����3?~7U�2�3e�~/���4/��eR��kl�$��G�+tO5�@Z�����3�u��}���y��ySU�'S�r:�R[�~���-���Z�fc�Zo��t�*>{`���)	��;s������
-��5]�Rkr?��uz�?�{��G���|P���Fq��pao�n�z A�vVo��/;�;�OC���v��"��`m���2�����.Vo�u1�����pP@r�l�������15������`��y)��-�1��ZEJ����hS�k��
^w/����F
�t���I���aj`kq�+{i�]A�l}�l��Zm����wW�Y��<Z�Tb���>����uyE������O��g�|���^����;����������rT�!YG�J������9n_��`�O��j����{{�|PA��Z*��X����>�f���G"�
����(�x�6/�A*\kp�E���^�wgR|���;`�2�?G��]��k93������5e���c|��,�$�T~�����:^$�&�B>�	�=Y��`������s��OS(�%B/�y�<Z���1�A�Ab����<����|��K���3!H=���'G�M�����T�^d����y��XG	j�g��7��o:f�9�����U�F
��Y��.I�v���qR
���e���B��������x�����.|@__�f���Ar��	c�]e�8U8T�~�p�JY4��S2���J�'���@���j��@pZ��`}�	�����~���>�.�%r]�������D��X�iF�1�7Wq�b�����dD~Zq/��|^E�/1&B�?��ex��q��9n�tt
n�/�)���i���d�
�G�"�ue��`(m��x���v�7K~���.O���ll���<�������(I��G�����{6�O�_�R����a��W�
��^`"�n����5����/Q��7�v���{yR�L�q���*0��Y������1�����Y�
��P���h����Ia��V5�`�����v r#�<�l��U5����������c�6�itU�U�:�'W�(Y��G7s�S�rb�H�I�x���F
�����,P�?j�����p'j6<�Jvg��d��L^0�b�%a�I����
�'�~X��"�,�m,�a�RT�J�S����
t�=����RQ��JI����{�2;Bs��wq=N�bD�h��!�*p�|����X��Ro5��������7;H�&��;�CdK����I����0it�N0x�OG1dTcL2�~��)]G`xtp0�>��j;�po���L/9���G���0�{
�/'�,���qm�"I]��+E�3ES�.U����R�^�h�{0���?�f��
��#r�������9T�t���g��}d�G�:��������
#101Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#100)
Re: Command Triggers

On Tue, Feb 14, 2012 at 4:29 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

An ack about the way it's now implemented would be awesome

I'm still missing that, which is only fair, just a ping from me here.

I took a brief look at this just now, and in general I'm pleasantly
surprised. But, as you might imagine, I have some reservations:

1. I fear that the collection of commands to which this applies is
going to end up being kind of a random selection. I suggest that for
the first version of the patch, just pick a couple of simple commands
like VACUUM and ANALYZE, and do just those. We can add on more later
easily enough once the basic patch is committed. Also, that way, if
you don't get to absolutely everything, we'll have a coherent subset
of the functionality, rather than a subset defined by "what Dimitri
got to".

2. Currently, we have some objects (views) that support INSTEAD OF
triggers and others (tables) that support BEFORE and AFTER triggers.
I don't see any advantage in supporting both.

3. I am not at all convinced that it's safe to allow command triggers
to silently (i.e. without throwing an error) cancel the operation. I
don't have very much confidence that that is in general a safe thing
to do; there may be code calling this code that expects that such
things will not happen. This diff hunk, for example, scares the crap
out of me:

-       /* check that the locales can be loaded */
-       CommandCounterIncrement();
-       (void) pg_newlocale_from_collation(newoid);
+       /* before or instead of command trigger might have cancelled the comman
+       if (OidIsValid(newoid))
+       {
+               /* check that the locales can be loaded */
+               CommandCounterIncrement();
+               (void) pg_newlocale_from_collation(newoid);
+       }

I don't immediately understand why that's necessary, but I'm sure
there's a good reason, and I bet a nickel that there are other places
where similar adjustments are necessary but you haven't found them. I
think we should rip out the option to silently cancel the command. We
can always add that later, but if we add it here and in the first
commit I think we are going to be chasing bugs for months.

4. This API forces all the work of setting up the CommandContext to be
done regardless of whether any command triggers are present:

+       cmd->objectId = InvalidOid;
+       cmd->objectname = (char *)aggName;
+       cmd->schemaname = get_namespace_name(aggNamespace);

I'll grant you that most people probably do not execute enough DDL for
the cost of those extra get_namespace_name() calls to add up, but I'm
not completely sure it's a negligible overhead in general, and in any
case I think it's a safe bet that there will be continuing demand to
add more information to the set of things that are supplied to the
trigger. So I think that should be rethought. It'd be nice to have a
cheap way to say if (AnyCommandTriggersFor(command_tag)) { ... do
stuff ... }, emphasis on cheap. There's a definitional problem here,
too, which is that you're supplying to the trigger the aggregate name
*as supplied by the user* and the schema name as it exists at the time
we do the reverse lookup. That reverse lookup isn't guaranteed to
work at all, and it's definitely not guaranteed to produce an answer
that's consistent with the aggName field. Maybe there's no better
way, but it would at least be better to pass the namespace OID rather
than the name. That way, the name lookup can be deferred until we are
sure that we actually need to call something.

5. I'm not entirely convinced that it makes sense to support command
triggers on commands that affect shared objects. It seems odd that
such triggers will fire or not fire depending on which database is
currently selected. I think that could lead to user confusion, too.

6. Why do we need all this extra
copyfuncs/equalfuncs/outfuncs/readfuncs support? I thought we were
dropping passing node trees for 9.2.

7. I don't have a strong feeling on what the psql command should be
called, but \dcT seems odd. Why one lower-case letter and one
upper-case letter?

In general, I like the direction that this is going. But I think it
will greatly improve its chances of being successful and relatively
non-buggy if we strip it down to something very simple for an initial
commit, and then add more stuff piece by piece.

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

#102Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#101)
Re: Command Triggers

Hi,

Thanks for your time reviewing that patch!

Robert Haas <robertmhaas@gmail.com> writes:

I took a brief look at this just now, and in general I'm pleasantly
surprised. But, as you might imagine, I have some reservations:

Good starting point, let's see about the details :)

1. I fear that the collection of commands to which this applies is
going to end up being kind of a random selection. I suggest that for
the first version of the patch, just pick a couple of simple commands
like VACUUM and ANALYZE, and do just those. We can add on more later
easily enough once the basic patch is committed. Also, that way, if
you don't get to absolutely everything, we'll have a coherent subset
of the functionality, rather than a subset defined by "what Dimitri
got to".

I share that feeling here. Now, given the current scope of the patch, I
think we can add in 9.2 all the commands that make sense to support
(yes, including alter operator family). FWIW, I've been adding support
for 16 forms of ALTER commands today, triple (implementation of alter
rename, owner and namespace are separated).

So while your reaction is perfectly understandable, I don't think that's
the main thing here, you've just happened to see an intermediate state
of things.

2. Currently, we have some objects (views) that support INSTEAD OF
triggers and others (tables) that support BEFORE and AFTER triggers.
I don't see any advantage in supporting both.

That's because you can cancel an INSERT from a BEFORE trigger, that's
how you can write an INSTEAD OF trigger on a TABLE. As a VIEW does not
support INSERT (directly), an INSTEAD OF trigger is what makes sense
here.

I don't think it's possible to transpose that thinking to command
triggers, so I've been providing for either INSTEAD OF triggers or
BEFORE/AFTER triggers. Note that you can't have both with my current
patch. It's a convenience only thing, but I think it's worth having it:
you don't need to read the trigger procedure to decide if that BEFORE
command trigger is in fact an INSTEAD OF command trigger.

Also, the code footprint for this feature is very small.

3. I am not at all convinced that it's safe to allow command triggers
to silently (i.e. without throwing an error) cancel the operation. I
don't have very much confidence that that is in general a safe thing

I was trying to offer a parallel with returning NULL from a BEFORE
INSERT trigger, which allows you to cancel it. If that turns out to be
so bad an idea that we need to withdraw it, so be it.

It's still possible to cancel a command by means of RAISE EXCEPTION in a
BEFORE command trigger, or by means of an INSTEAD OF trigger that does
nothing.

to do; there may be code calling this code that expects that such
things will not happen. This diff hunk, for example, scares the crap
out of me:

[...]

I don't immediately understand why that's necessary, but I'm sure
there's a good reason, and I bet a nickel that there are other places
where similar adjustments are necessary but you haven't found them. I

Might be. That idea was sound to me when under the illusion that I
wouldn't have to edit each and every command implementation in order to
implement command triggers. I'm ok to step back now, but read on.

think we should rip out the option to silently cancel the command. We
can always add that later, but if we add it here and in the first
commit I think we are going to be chasing bugs for months.

Ok, I'm going to remove that from the BEFORE command implementation.

What about having both INSTEAD OF triggers (the command is not executed)
and BEFORE trigger (you can cancel its execution only by raising an
exception, and that cancels the whole transaction). I'd like that we'd
be able to keep that feature.

4. This API forces all the work of setting up the CommandContext to be
done regardless of whether any command triggers are present:

+       cmd->objectId = InvalidOid;
+       cmd->objectname = (char *)aggName;
+       cmd->schemaname = get_namespace_name(aggNamespace);

At some point while developing the patch I had something way smarter
than that, and centralized. I've not revised the API to get back the
smartness when breaking out of the centralized implementation, because
those elements only can be set from the innards of each command.

The API to list which triggers to run already exists and only need the
command tag, which we might have earlier in the processing. Note that we
have to deal with commands acting on more than one object, as in
RemoveObjects() where we loop over a list and call triggers each time:

https://github.com/dimitri/postgres/compare/master...command_triggers#diff-19

The best I can think of would be to build the list of triggers to call
as soon as we have the command tag, and skip preparing the rest of the
CommandContextData structure when that list comes empty.

Still no general stop-gap, but that would address your point I think.

Now, about INSTEAD OF command triggers, we still need to implement them
in exactly the same spot as BEFORE command triggers, and they're not
making things any more complex in the per-command support code.

I'll grant you that most people probably do not execute enough DDL for
the cost of those extra get_namespace_name() calls to add up, but I'm
not completely sure it's a negligible overhead in general, and in any
case I think it's a safe bet that there will be continuing demand to
add more information to the set of things that are supplied to the
trigger. So I think that should be rethought. It'd be nice to have a
cheap way to say if (AnyCommandTriggersFor(command_tag)) { ... do
stuff ... }, emphasis on cheap. There's a definitional problem here,

It's as cheap as scanning a catalog, as of now. I didn't install a new
catalog cache for command triggers, as I don't think this code path is
performance critical enough to pay back for the maintenance cost. Also
consider that a big user of command triggers might have as much as a
couple of triggers (BEFORE/AFTER) per command, that's about 300 of them.

If they go crazy and have more than that with special conditions each
time, let's say that's 1000 rows in the catalog. I don't think that kind
of size and use cases (DDL) calls for a catalog cache here, nor for
micro optimizing things.

too, which is that you're supplying to the trigger the aggregate name
*as supplied by the user* and the schema name as it exists at the time
we do the reverse lookup. That reverse lookup isn't guaranteed to
work at all, and it's definitely not guaranteed to produce an answer
that's consistent with the aggName field. Maybe there's no better
way, but it would at least be better to pass the namespace OID rather
than the name. That way, the name lookup can be deferred until we are
sure that we actually need to call something.

I'm confused here, because all error messages that needs to contain the
namespace are doing exactly the same thing as I'm doing in my patch.

5. I'm not entirely convinced that it makes sense to support command
triggers on commands that affect shared objects. It seems odd that
such triggers will fire or not fire depending on which database is
currently selected. I think that could lead to user confusion, too.

You mean tablespace here, I guess, what else? I don't think I've added
other shared objects in there yet. I share your analyze btw, will
remove support.

6. Why do we need all this extra
copyfuncs/equalfuncs/outfuncs/readfuncs support? I thought we were
dropping passing node trees for 9.2.

I didn't clean that out yet, that's the only reason why it's still
there. I would like that this work was not useless :)

7. I don't have a strong feeling on what the psql command should be
called, but \dcT seems odd. Why one lower-case letter and one
upper-case letter?

Well \dt and \dT are already taken, so I got there. Will change to
something else, e.g. \dct would be your choice here?

In general, I like the direction that this is going. But I think it
will greatly improve its chances of being successful and relatively
non-buggy if we strip it down to something very simple for an initial
commit, and then add more stuff piece by piece.

I had the feeling we did exactly that when reducing the API not to
provide the parse tree nor the (rewritten) command string. I can see
more features to drop off, like the ability to silently cancel a
statement in a BEFORE command trigger.

I'd like to keep the INSTEAD OF command trigger feature and I think we
can support lots of commands as soon as the integration becomes simple
enough, which is where we stand as soon as we remove the statement
cancelling.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#103Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#102)
Re: Command Triggers

On Wed, Feb 15, 2012 at 3:25 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

1. I fear that the collection of commands to which this applies is
going to end up being kind of a random selection.  I suggest that for
the first version of the patch, just pick a couple of simple commands
like VACUUM and ANALYZE, and do just those.  We can add on more later
easily enough once the basic patch is committed.  Also, that way, if
you don't get to absolutely everything, we'll have a coherent subset
of the functionality, rather than a subset defined by "what Dimitri
got to".

I share that feeling here. Now, given the current scope of the patch, I
think we can add in 9.2 all the commands that make sense to support
(yes, including alter operator family). FWIW, I've been adding support
for 16 forms of ALTER commands today, triple (implementation of alter
rename, owner and namespace are separated).

So while your reaction is perfectly understandable, I don't think that's
the main thing here, you've just happened to see an intermediate state
of things.

I'm just saying that nobody's realistically going to be able to verify
a patch of this size. It's either going to get committed warts and
all, or it's going to not get committed. Decomposing it into a series
of patches would make it possible to actually verify the logic. I
guess on reflection I don't really care whether you decompose it at
this point; the parts are pretty independent and it's easy enough to
revert pieces of it. But if I commit any of this it's certainly not
going to be the whole thing in one go.

It's still possible to cancel a command by means of RAISE EXCEPTION in a
BEFORE command trigger, or by means of an INSTEAD OF trigger that does
nothing.

I think that there is no problem with cancelling a command via RAISE
EXCEPTION. It's an established precedent that errors can be thrown
anywhere, and any code that doesn't deal with that is flat broken.
But I think letting either a BEFORE or INSTEAD trigger cancel the
command is going to break things, and shouldn't be allowed without a
lot of careful study. So -1 from me on supporting INSTEAD triggers in
the first version of this.

I'll grant you that most people probably do not execute enough DDL for
the cost of those extra get_namespace_name() calls to add up, but I'm
not completely sure it's a negligible overhead in general, and in any
case I think it's a safe bet that there will be continuing demand to
add more information to the set of things that are supplied to the
trigger.  So I think that should be rethought.  It'd be nice to have a
cheap way to say if (AnyCommandTriggersFor(command_tag)) { ... do
stuff ... }, emphasis on cheap.  There's a definitional problem here,

It's as cheap as scanning a catalog, as of now. I didn't install a new
catalog cache for command triggers, as I don't think this code path is
performance critical enough to pay back for the maintenance cost. Also
consider that a big user of command triggers might have as much as a
couple of triggers (BEFORE/AFTER) per command, that's about 300 of them.

Yowza. A catalog scan is WAY more expensive than a syscache lookup.
I definitely don't think you can afford to have every command result
in an extra index probe into pg_cmdtrigger. You definitely need some
kind of caching there.

Or at least, I think you do. You could try pgbench -f foo.sql, where
foo.sql repeatedly creates and drops a function. See if there's a
significant slowdown with your patch vs. HEAD. If there is, you need
some caching. You might actually need some whole new type of sinval
message to make this work efficiently.

too, which is that you're supplying to the trigger the aggregate name
*as supplied by the user* and the schema name as it exists at the time
we do the reverse lookup.  That reverse lookup isn't guaranteed to
work at all, and it's definitely not guaranteed to produce an answer
that's consistent with the aggName field.  Maybe there's no better
way, but it would at least be better to pass the namespace OID rather
than the name.  That way, the name lookup can be deferred until we are
sure that we actually need to call something.

I'm confused here, because all error messages that needs to contain the
namespace are doing exactly the same thing as I'm doing in my patch.

Hmm. I wonder what happens if those errors fire after the schema has
been dropped? I suppose the real answer here is probably to add
enough locking that that can't happen in the first place... so maybe
this isn't an issue for your patch to worry about.

5. I'm not entirely convinced that it makes sense to support command
triggers on commands that affect shared objects.  It seems odd that
such triggers will fire or not fire depending on which database is
currently selected.  I think that could lead to user confusion, too.

You mean tablespace here, I guess, what else? I don't think I've added
other shared objects in there yet. I share your analyze btw, will
remove support.

And databases.

7. I don't have a strong feeling on what the psql command should be
called, but \dcT seems odd.  Why one lower-case letter and one
upper-case letter?

Well \dt and \dT are already taken, so I got there. Will change to
something else, e.g. \dct would be your choice here?

Yeah, probably.

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

#104Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#103)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

I'm just saying that nobody's realistically going to be able to verify
a patch of this size. It's either going to get committed warts and
all, or it's going to not get committed. Decomposing it into a series
of patches would make it possible to actually verify the logic. I
guess on reflection I don't really care whether you decompose it at
this point; the parts are pretty independent and it's easy enough to
revert pieces of it. But if I commit any of this it's certainly not
going to be the whole thing in one go.

Ok, I can perfectly understand that. The principled implementation is
not saving us here, we still need to review each call site. The way I
read your comment, I continue working on my big patch and we'll see what
pieces get in, right?

I think that there is no problem with cancelling a command via RAISE
EXCEPTION. It's an established precedent that errors can be thrown
anywhere, and any code that doesn't deal with that is flat broken.

Sure.

But I think letting either a BEFORE or INSTEAD trigger cancel the
command is going to break things, and shouldn't be allowed without a
lot of careful study. So -1 from me on supporting INSTEAD triggers in
the first version of this.

I'm sad about that, but I hear you. Removing.

Yowza. A catalog scan is WAY more expensive than a syscache lookup.
I definitely don't think you can afford to have every command result
in an extra index probe into pg_cmdtrigger. You definitely need some
kind of caching there.

Or at least, I think you do. You could try pgbench -f foo.sql, where
foo.sql repeatedly creates and drops a function. See if there's a
significant slowdown with your patch vs. HEAD. If there is, you need
some caching. You might actually need some whole new type of sinval
message to make this work efficiently.

Ok, I will test that down the road (before the end of this week).

I'm confused here, because all error messages that needs to contain the
namespace are doing exactly the same thing as I'm doing in my patch.

Hmm. I wonder what happens if those errors fire after the schema has
been dropped? I suppose the real answer here is probably to add
enough locking that that can't happen in the first place... so maybe
this isn't an issue for your patch to worry about.

I get it that I continue doing things this way, get_namespace_name() and
friends are trustworthy as far as I'm concerned.

You mean tablespace here, I guess, what else? I don't think I've added
other shared objects in there yet. I share your analyze btw, will
remove support.

And databases.

Right, on their way.

something else, e.g. \dct would be your choice here?

Yeah, probably.

Next version will sports that.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#105Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#104)
Re: Command Triggers

On Wed, Feb 15, 2012 at 4:32 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Ok, I can perfectly understand that.  The principled implementation is
not saving us here, we still need to review each call site.  The way I
read your comment, I continue working on my big patch and we'll see what
pieces get in, right?

Yeah, I think that makes sense. I'll have a look at your next version
when it shows up.

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

#106Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#101)
1 attachment(s)
Re: Command Triggers

Hi,

Please find attached version 8 of the patch, which fixes most of your
complaints. Remaining tasks for me here: closing support for all
interesting commands, and improving docs with a section about command
triggers in the general trigger discussion in doc/src/sgml/trigger.sgml.

Robert Haas <robertmhaas@gmail.com> writes:

1. I fear that the collection of commands to which this applies is
going to end up being kind of a random selection. I suggest that for

I didn't change that (except for shared objects cleaning), and I still
think it's not too much work to add all objects we care offering command
triggers for in this release.

2. Currently, we have some objects (views) that support INSTEAD OF
triggers and others (tables) that support BEFORE and AFTER triggers.
I don't see any advantage in supporting both.

INSTEAD OF command triggers are now gone.

3. I am not at all convinced that it's safe to allow command triggers
to silently (i.e. without throwing an error) cancel the operation.

That's gone too.

4. This API forces all the work of setting up the CommandContext to be
done regardless of whether any command triggers are present:

Fixed, I've spent quite some time refining the API.

trigger. So I think that should be rethought. It'd be nice to have a
cheap way to say if (AnyCommandTriggersFor(command_tag)) { ... do
stuff ... }, emphasis on cheap. There's a definitional problem here,

The CommandContext initialization is now doing the index scan, then the
command context properties are only filled when we actually want to run
some user functions. Both BEFORE and AFTER command triggers context
filling are now protected.

5. I'm not entirely convinced that it makes sense to support command
triggers on commands that affect shared objects. It seems odd that
such triggers will fire or not fire depending on which database is
currently selected. I think that could lead to user confusion, too.

Agreed, they're now gone.

6. Why do we need all this extra
copyfuncs/equalfuncs/outfuncs/readfuncs support? I thought we were
dropping passing node trees for 9.2.

I still have to clean that up, and as it's not as simple as cancelling
any change I've made in the files, allow me to still defer to another
version of the patch.

7. I don't have a strong feeling on what the psql command should be
called, but \dcT seems odd. Why one lower-case letter and one
upper-case letter?

The psql command is now \dct.

In general, I like the direction that this is going. But I think it
will greatly improve its chances of being successful and relatively
non-buggy if we strip it down to something very simple for an initial
commit, and then add more stuff piece by piece.

Having now removed support for the ability to cancel a command without
raising an error, and after some API cleaning, I think it's now much
easier to review the patch and grow confidence into it being sane.

Also, I've added regression tests. They are only exercised by the
serial schedule (make installcheck) because they would randomly ruin the
output of the parallel schedule and make it impossible for pg_regress to
do its job here.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

Attachments:

command-trigger.v8.patch.gzapplication/octet-streamDownload
��;=O�<mw�����_�'O�%!����Ms.N��R }9����]cS������?3#���
$�����
X�F3�y���L���h�D�<�}�8����=���
��q8����&;��;��cf��vfM�����y��NN��������/���������E�Vge�xUc�������v�7
Z�����F����1`�_���������a���}k��3F����3���X���g������J��f�"\Y�g;��{i$���"?� �~�o����������x��r��f0[���Yr��~Y2���a����u��k]c�V�'`��]���/���?���m�,>7=;��]�U�U��gZ2�\<f��>>��oSB�D�Ac��p�{�������z@j��
��H|���
������r�K������qk�G��^�@Y��O�N��@���B"Rc��o��V���qd���~/�z
������[#
:�����t{�����wF����k��jh��������7�Q�jl�oo[�D��`����

�vw������B��qw��F{��y���}zX��X4�nq�il
�|DKF<�1-�\N��4#E[��L<��cd^��y0iV�A��R�+��z��e�M�C����u��sZ�)Q@z����s\`��K��h�!�S���������!#p1[�N�uf����Zw�����yh�9�S�b?Z��)�����d�I�5D���H�'k�Bw^�u��{�//������i
G���E���8\y�"tB~��NU��|@��aW�^���r�V����)�����vxh�B�U�I��K�W�G�U���tppy����Cf2�d*�o��	�la�E_$��C�19�#�9�r�4E�?`�F��"����P>>�	�_�q��p��x���������eP%Y>����nl�&3�$1?/Yj$����� d�<�h���b�28W�V+�q��@�M�J&|�$ps
4�b�`B��j�v�/��iC4"�o���Vv�a�)�e��G��:0��^}��}�S�v�\��%(1:�U�Y������p���.��A\m=�+�_S4wy��\V�E�J���F�2�@���9��z*� ^X�z�E*��3d|�XSo����k���:���D<�Y��:���)����=�����j�,�V���{z��4Y�Q���/>�x�)�h�2��Q�\,��Y��9ksXv=frx�j��,ND���m��Ot\����������%6����K�i����SA�K������a�.d��	�����L���c�-�
�H1LaK�r099|;K�F��L��oQE�U��"T��k�`���)[��`�"�����HP�h������)l��Z��*���H��A�0���Xd)%p`��3�dML��	��.�xt��9{��k
Q#8��y�!���/�U�b;j�<��j���e��p0�!��E��\���/+	>��������������>�tn�dr�5Y��E9���d}��Cb
�9��Nbh=r�$��?;nEAa�!����*��L ��6#x�]�����5@+�I�q���P!��v�m4���e,M�aU��c� ��������/��5�+��)��-���_R�3Z���?U
�A���X:|���"�g��D�7S���&����p%��tn��_��L�*B�������G�9�a~@3N�V�/M�5G_7�OJ-��J%�7�,��a���1�p��W�(��������?!�bQ�m��/��~I��	���/�Q��f�l�:���/����m�U���Y/\/��k�S(�r��-�H�d���{���>�q�\��+�M0+��=��F����7��`�=]�9d|>�O|��.��5��6�7+%�*�������n��^����
h��c�_��}c�DzM����:�Y]am�k����78�	���X�r�}����Z9b(P.V�,��R�p�lTj��F*�����*�B
jP<������K���{?�f~w�L�@�����}},�`���y[�A�r+���Y`�G�(���7���,L��'���g(�(&=E��
K�������xx��'$�S�O'��?��C�������������[��4��G�`����\&����H;�[O#���Yd}jY�'���qq�l�l9�\GSp�H�v%�i�$�d�`G)j�����Vq����Z4���p4d�<��D�eeD��k��V���i��p�S��I���6j����9���~������a��jz�����}+sF��Y��|���h���@'���m:����/*�����:�����sOst2��%�s��ax���6u�c����=�*����nQ���A��c�..�a�������!���|�)(������N~�"����]Hg[�����&���Y�e*A��4���;�&���{�1$t$j���Q}���pk+������:k�!�;�,��Dw����n;��5���hx�����v��a4�^��=����I%y�=��VX�+�$S�
!7�����(f|��@�����\$��O�x#�`�n��Y�����,X}m��������B��`k�����|[,�dc���3���]��j�7��u^��k��t
DHJJ���?����~��!��}Q2m{[��
�G��R�		��Y���1������llFQ�����������E��� �u�P=�R�W�r��$����9w���'�!�������V2�5����q��7���2�8�Dk��#� ��'\G�+�4L�% �V 1+{Fv��ht�I�������$�{K�t��\g��<�0��HP(S-{'��w�8�:I�
�����������-�����	�QK�O+�����[�p�Y��U��:����W��s��f�j�����&��x����Q9��g�{�������}'H�i���{5�)�������x�*��� E����M��q�9E�������\,H;������bN���������Z��������h<kg�Wh=i�[����X���l���~�)����������;3o���Y!������L@�e~1�{y��w�� m���� ��n����K7jYn���q�j�"�����v	��'g^��lo�����>o�uT�����|����&�[��y"��p�#��d<1�e�C�R��Px�I����c��e��=
P��}Ma���������-���`��!��\F�:N,��G`��g1E�X��L�|����
����H���Z�e�159T�{BYA����'.�����}��?ivj��/]�{!���[ UZ�+Y�|uZ�4�R����g������������v��2$Z��-_u���=_����zV!v",�f!|��ps1Z.\�-�
v�|�2���H�����Ax��y�<�WX��`��G��o}��a�C�������z�K%l�R�Z���	���%������ui�C0���d7�����uD7<�,�����$��
W���4�3����ZrE�,����:)�N�r\�g.@������j��6�r�O*����� !s���?�����x,W����r2��d�Am>h-^2`��B���(����ryS���C"h ������
G�����IQ���/J=��@@���++�I*��V���C����D-\�)C:��zS���d9��P����n:Y�n.�\gkSb�y�2&��g�O���&��X�0,��G�Z��M�r��k:���4�R&�BV�T���5,��[��s��U:B��I�
E����a1��0 �@�������Fb�&����8��8,�B
�%�Va\����ao!L�p����@46&S�h}O���?%����y���'����:����p!����j���M� 2�-��&�g	�n����1��9�8���:�2�-�a0�7�}o��1�����tJ^r���**m�4���5�a�7���z��h���;7��x�fIr�$����(��"�nL"���s,�=w�x��9���<���$�W����j�g5���#����:-��%0	��4��t`�;���B?������ ���Q�e�23V�AC����N�-X�I�w�q��-�)�e��T\;���=�]`Z������J�#������n�3�SA�������H���m����������dm�u3�I���:�b���d�5^��R"`�!�i�`�v�A�%-��M��n�A�&V|(��@i����<%�����@-*$��xo�
Ii�H���$�0�����y3���{����+|�(�+I�:����}QqE�QUt������S��dP.'���7�������^���*�����J�VX��7z���i���Z�f�'q�XJY\.r��[��nbF�(�h��@
���$`�`�5 U������U������x}';���h�u@i��5;��[�OV���O5���1���7<�,7��1�s:��������jD�i�e�n���3�D�zD�G�Q�m���r6�R�*Eo���m���v:?�mu�����o�m���-$��:�������v�5�k���c0�B?#�)����H������#~g�J�f���>`&x"�^���0����N|a�ow8���=N�;Oq9eQ��Z�����,��(����G�7���A��L	F+P�a%�H�2�����+��W"2N�Y=az���[����/;#�	�H�Ti��]\R|n��#%0�!�N��f�^���\4^��p�����ip���s:g�a+ew����$x��O7��~��ef�����7�����x0������>
c�i�����N[�xC��8�l�q����I�&Z�fR[�C�<���e���-�>�V��C��B\a���������L+f�T��w�\�'*2�x�G��!�\�X�^�SU�~V	N�_��7�ZMi�.^�����,Dc����k�?�`Z���;u�y���aa�TV������o���R��3y�>K _/�`�UN�Y��W��U��'�	���iX6Z=
�\�i�_�)�/X	C�7���y}������e����G|�[�G'�Y)��x| k��-��B�������:T�%��~�9������R1�������R�l.E������N��#����D!��3:���i�l���r{�������d�q�9r+�T�$��W���a��~��U���S��i���������qJ�y�"� �|�=4YE9���W�{Z�	�"��%60z�;�����t ����������C�]�nz�������0�s	�I����!�����U�rc��h�yB�N���sd��\8i`	R?H�9���c�y����O���o��h��9"�6@Az��9��0WiR��ws�r��T|��Z]�O�|�Si�==�[�	c��$�p���oz}���
}��$������/8���H�k�*}�^G��od�Y����'�Eg�5"0��K3$���{�
s���R���dh�s(3�%Q�-?k�n����$\a)ugE�-��D�w������O�����T���"�6��*�h�A������M���[��b��tyR~��D���\����/R�Cd�K��������6��u�uz�_.���[t�<�/=�����}m�����S�x������ {'�( ;�`D'�����H-�XH�Z2a&����z����%p&s�x�	��^�j���l��}��n�^�[����8���&N�A�
��{
���EM��8>���D��T�����II-���*d�.��]�W����/��]�?�F���#�������[t?�� >�2iE���6\I���X�`vT�.�����=dZ��]��)m9o��X�[�j��~[-��*�u9=�oR�O�L�������'T@���g���U>����Q�����b��L��H>�O���~������I�f&��\�W%�G�{���>Y��Of���(��7u�md���x��o~������a[&�/7k$�
��y�#��4���"j��v�]��x���X�"VI\q��S�FD3R��VL��0���-��w��=�b��F�	b3�Dk��#��[�����!�B�]�Q�X��4f`�u�]��s��Jz,�mx
U�UQLE�='f��W��F���e4�e4�*<hz�%���\:^y���Xd���n�����!�������_���R�a��[�������Q��v;^5�d3iE������m���p��
�LB�[������O7����`eo��[���*._FH�m���"�3�J<�Fd�A0A��v@���?�6�G ��t���N�_���q�K��"��-#�������_�����ESPz�`���T@��+��}��v�W���3���L���(�,6�>���]U�j.�x�Iw�������Y�A�X�xx�Ey��9����1j�JXVd��YU*f�������Z���w{A��z��,���3B�Y,��'[���Y��p�3_8����N!�g�W�AS��d��p���q�����zE��=���p���B��
�k�]Z�LU�
��^�t �R�#J^|�R�-�B�&{��#}A��R�qUO�%vS�����C���O@L{`NVYX��U�%�`��jQpB2 �=#�B%�T�����7��S�P�����
�b��e�'�n�cp��&���x���HcU��-Q�|�B ��z�F�1`@a�L\ 'X����2��Q�z%]iq��v�e��A��W��6*�U�0�LXH���Z�UR���w�ko�&�[�����"��(1�Q��c_�����2!"��(�0�)���lt�,{FGds���K��X��3����y]�WT�YiRpNV�0{b�.�Y���J�+4E���8��"���b���S_S�R��	�YmCV�n��n;p��B�,���a��?��"��������e^R���aT�a�������0Z�O�4�����V�W��g�i�l\h�*4�1MUa�Y�m[h����-��>	^JsC���!h1/	�uTp�O��<�����Lb��k�����)n�5�7a�1����
�_F.�k���6� ����|�ls�f���tmq�K6��u��4��LK7�����$k��LK6�����,Nm�o�C�i#���%��(y�&X"s%��s�lQ��v�" m{�=�����
i���G2�)��Y�-W��p��<%�����������,�����b�>��yG\)
�`+:=��.�^���)�D3;�`/�� �[!�/&�_k3#��L�*��D�DDj�P���U�D/��y����n�y���p�R�lr��t]�&�X���s348��c�n	A�ijY@'��N��CX����r��U��O��d����H�����>:��{�����HZ�"!���|
>r�������?H���T)���7�#�H�?���o��K����h�_Ct(<}��L)���5>-���U��(eE�A�h����Y�����WH61pTQG��k6H��	������z�=�(G]���Q��O�~w���^m���/�<��]L8Y(�X��^\2� �>�q�b ��NU$������)-a�����,bvY��&o�6(����YcT~�`o�>)tR����
��J�����<���A>K��������8�=�B��Mz\V���x���tL��w��f�Q~�H�fD��p��R��r5�r5���	���r9&��E���\c�&W��c���O�H�b�gK���G[�j�,&>�����-;���R������<�fsPXB.�4%��\Er��`z3��������`u��BW�0���,��qa,�N���(b*`�5=%�V�
�$A�ew�(�M����FZ�v�Z�RF(����������I6)���k�X���h���]�f��G�e��*�)�4I5NSd(�vo�h�k�`��j��"�'��0P��(r�A�L�/�#$��n�R5���(t
g0��W���4���<�|�L��"�^.�9Y�|����u28�zO���F�W>'U�i�\A�~�?u0{��j2���;[�[����s�)Wb���bxf!��a]C�
���b!���.��p�_(�'�>pM�@E&Ie�_��e�[����!L�`��1��Nq���2�H���VkO]��#J��_�+w�}�U��W�=�,�{���c�S�FkL�A��')�����[�;��Z,��'�&.������W�z�z�Q5{{��t�Q�1�-� u��������hpzy���)-L'T��Q����5]���s�,�1�K�%�m�@�����y�D��)�F�w�8G�;b2N�h�cw�'���,s��P���U�6�u�pw��B3��!q���������1����B3*av��D�mp��8������������%���|
8@3fK'��O2������Y���6�Q�8����c/�(B��x���3�U\!y�
B
��=���T�<A������w���7W��s?�J�7PR�aJ�U��&\�j��������?��{@# <���j���U�Q\�AT�}��O!�E��Bi���_�����;�&_A��6XC���hIr���T��D<^���K������=%_�:�yO�qq��Q������&Z��=^��^O��S%P&�-ig�XA>h��,�R�H8 �\�����5���h
j�A�F"�N/��1�~��"����5��]�r�Zqp�DD������D��SA���&~�
$�;$kuWZIKd�\-W�-=��n$6`�R���jOd,7A%�`]�qUv:��G�����k�F:���k��rq��C�Vx��	JD]=���b
4�����T7������)�r(�]/��?���
Z3qv�,���1d�������)����3�]��3�Z>nT=]<�p����iGy��U���k���]�0� C�z�
H)�@�t��6{#�ac���Qu�t�3F#�=w��jm��=�?���#S�z����[�:j�H�{�
p_��
|�sS���CG����qGq���m
K1b_�Q��^�u��Q���G��J�V��$�zP�X?�����X�0��������A��l��M����(Lmuv�k��!���|�1��K|�({��{��6z����u��p�zZ#�"���\�x_?���c*�������k��6x�x�Z���_';�������^�O�'�>5��bga>����}����Y�
Td�xe_"O��pB��0-�D�@8��3����f8�w>l��x�8$tB�{u�i���RC`�ym����%�\�K��P���`��j�Pf6k
B��u �G�����T���8�lLS�r,}��X���W]�S����6^���^�������c�o��Y���?�X{�0k�6X���gbk����?�7��<2��9�����;oHL��			����S�	f��4�O|��f���0A����Ke,e��%�jY���Nt��H���x����������1k��V!�a�s�����/�� ��G(a|YX����
��m&��A0'��F���� �l��b�P����{��=rL�*[ ��)R����A�=l@������*� f����M���f�+���~�����k��'/��;��D���|�}@�x)����t���\
��4Zz-���*��"@�+X��ms#��F���}s?K�!@X���sG�\y>����-�lzz	��(����[#n��������H*Po�O�A��2������r�A��e���M��S�<-�z�\�#d>���������a*l���[���D���v�@��:�V��|?��PQtyL��5���Cn��=���k@c��0�4uZ�N���"�d>!��h�T|2��������i��@�
�_p�h�k�; %��e~=Zp�;k�?Lr��A�
I,�i�A4�������|���	,hQ"���!��5�F�U����G����h�����d������~fk��A��������0Q��fE�����l~��JV�`�@��ew���������-�bI�R���HC*������&$r�
��q���a�t��Fs����>���{[wUB(�9�	�L)m����G�$2�h��80)�����7<U�(PaN�������I�f������|�~��D�J��0�N�[����D����_�+�����J������d��]��������yw��y�W�m��[�v{���:��"��sZfup��Q����S�����q�=dJF�

a�90m�hI�-)@M<k�e}�\��HG���������sl���T�L��a�oJ4���g�� �w�zD����H_;|���S� j�CL+�O��=)���h"*���
mM�j.EM�y��V�Z�Q�?b|bk�i��r+����`Jj�+AY%�]���G(U����f��z}C�� �i:����� �|���3�����o���n���<>����P`8\�X�Q���)t`(C�|�<�U��}B*-��N��{�_|������i�?�E����V��=;`�y z��1�w@��E��==���\'%��>C2,?.uAg��-��J�|O=-q���7@����� ��.������S��N���*���\��Q���c6.�x��,#��w�C�K+q;��]�����U�c�|�-��K���A�s�?)+'���X�y���Vb>���?�Z���XH�&t.N����x��5)k6���8TB.1��@�>�-8���"�������C�3�f�b�����<�,�A��<��V(�����h����
:Q�o���[������FWL�\ti��#9���������c�0��eN���x����I�ft�&t6�� "�t��k�����9����g��*X��C�J��H<D'��C7D��,<=X��-��.H3��i���jf�cE\q�h:�3���$XJ��H���B�s�I��:�cw>�:�`�t8�'{���9B�� �M�|��31����2�U/t�rC(?����$�b7V�3��h{p=�{�-�s�������ua������.JR�����T`!PT�I	b'��&�CU/�X�������I�O�����_����Q@��yW3]����CR9JdT�����|�<�������:f`1��rx4|�5�h��7��s���3C�*=�UM�b��8�����i�z4�������=��`xH�.]R��(��
c��N�-X����
��A-.�`oO�u���!����G�d��N������L'�/� �n8M��h�AEJ����?���)�_����"R�n�{E����(�'~c����zu<�.v��P�@A+�{(��$N�����B,kd�z���t���P�M�)��Mp��/%�53��F��
�Y�4��q:x����Fc�!!t��h%U�����4�Sc
�]�>��n����w#�w��pxy��kz��^N������^�j|#n"o��@�*2	�k�1K������2��ZzS?��G���ymN�`m��x6����}���	c������C	�c2�������f���q��[�L����D�S���=A�M��<B>��]h]!�6�fNyC�S�|P�'*b6�����;����k1�kr�^�0����5�����P7�����~��$��	�,�M"�ItO��P�_��j�]�� n��Qf��bO.�����",���2�V���>�$�x��w�x����>@�� \@���d!�_���4+G����
����X��jb!J�!V���gUV�.��8�}*3{��ccw,����.G�|IB5�����>�t�������}�?.>�6\�;#�7�������gF0i���<	�Kq�Wht��m6,�Y�6���	b����j������foBq�<��axj=7��Yr.����j�%�,�}���:6�|����4���t��p)f��yg�Rf3"�$<��=+��HZ����CO��X�/�����20��������i���O����"y�x��0������������'qi}Tx���U�.f~x+
�q��]�$�}���l��WC�4���j%D a����F�a�k������b�����'��w����� ��,�Vz��;��c�wBJk��H�\TR�1{���[Lg�����_bd"��Y�����^�&������d�w��$/������Z�G\$u������@�t��	t�U�������x��y�E0��
��x`��&1�m���nf����T�xwz|B��ha>S����>�SdS$��
L����s:3���h1���fe��	<
y����D}�g�hOJ��gh n����>I��?j�U���;b��h�+rd"7T8"��A�����UR@������+ft<��6�RUT
�"x	��#�%��p�V	c��}�|�|�[|_��NgR0�����#O�<U{.�7P����@c��Cl���%6��������jj��'b��m;��`./Z<;�0���Q���^v)UW����up{T$��x=cwW�L~�=�VpTL:AZ����j����F��KcCT�>�����O%G�LH��]m���2�dD�|^�^��N3������������bc)����pu���*��o&��	��0�$�C62J�
Q<�0@�r��+�l�
rZp��q(Z��/����B�F����$ �$��E:|���%���F���"?��BPt�h�V�/�w!u'��E�����M��H@"�]��5� 
abN���N�6����T�5��Z�����g�6�O������7fB�d&�/�����>��T����3Zl�9Z8]j�,�}�h�,g����j�������~�yp!�����HF�xGY��bU1r}�4�2��v��MU��4QK�G�8��$9k�Up+S���;����yf�d� ��gFr��hd������.����z$���_������YE���&S�w� T9G��{��gqg�,�g3������������%�m����NY_x�*t�J�j}����b�f9L�v���D_�:����x3�y��.g�aQv( 7�+��6��*�m��%��`��b���f<���t����bJ����R

��$�p�����[1!x1�b��P���<���]����JQZn��������Z�O��f��
�K��Oa`�}��*Z�JY������n�V[�:e|��{��Y�O����@��auUlK����af�Er%SA��\F��O��%��f���~3�)��Q�M �.��@[U{�Uu��6�^)�k/����:m�_�C����2P-�
X[.E
��r?�����S-hz�q�`��o�WPz��np��Y�Ai�a��q���	/;c�~s�V��:*r�����+Z�f�8���������W5�S;��S��&2{�N��$��S6�`>�l��)���A�BM3��F�m���d�9.[��{���	�oVRv���`�3-|�G��n��O-����7*G���[`n�k�� 9������l������R����-���� �{�1��h���k�F���-���R,��@�g�8��\M>[��h���&9}�I���xi����0'JE�������,: �X|��Q�3^�W��	F~���� >�=pd�n^����r���V�O�b_�����UXAO�4��I���D�����#W�� oT0����S9�Z�&��k��
>+���<�����GIH2.L%�z�p�8,����wynd�k5�F�G��3�R+�m.�R�<��Y���S�������4o3�a�������%-�g����5k"��K������h&s���}����n�]g���)���
���%
��F���ZY��|��3gI�e��B�p�7+On�ca��O��v�V��~����z�����ED��H���J��La��SU�����xQqq���F�&��~�R1/���V��ci���MVp��da	�[�&��,�[���B�q��g������S��$��b���Q�V`��������i�[m���-I6����%�J�
b��N$'S@&g�e,"�+��<qI����=��.>i���(����-�K��X�b�?)���:��$4��\����L��o��J)wR�r��4Wg-�k�@�����h������
��K(���<�XnK6� �l��i�\@6���%�k#��������B�x�&�n����������_N:�\��D8�����y ������G��hc�y����������91;�wC�����Uo���C8���-���n%���q!N�-�k��j��@,'�KH`�g�Z��-�I�h�sLY��"-p�n�"k�{$nd���K�R�U^EA��T��dIfX$�
��:,�j7���V�VkM&��~Y�d��z�{��v/!���������9�,�e���S������g<�bB���%�����B3�$�i�����V��.��t5��*�o������J���N��|x����)�d�++N����G2�U���di�1��	�B�`�P7����?i!�X(=K)��Hr�����zr����,��-��5����^�>��:�i�`�)����,$����7���`U��e���b)�`�%3$�H/v��-n�����R+���}.p2zy��� >'+��#�����D�e��io$�_#w5���*C��<g��V���kP��a2���J �mv�w�m�>6
�0�!�X�Z��4b$�Y�1Y�N�7y8	���]o���keA��������.:w����ri�"�u�]�/!\1�a�9$��x;����H	o'.3�<,�M�r��<2AJ���)��(�>���Up���+�IW�k`���&����0K�k��~�E?�{{?��,�&P/R�\5:Hr�f��-�	bb�l�Bi1	=O�P�%��7S�����������~
�J;�Z�������L�����J�2�(�x���(�0��}���;R+�(������g��������A-V%��G�>�����i12^s�)�;�������`���c9���� ���'�D�L(rg�15@�Pu���[�*H�^{����������W��b�
�,9��u�������&��I�f@��B��@�`�:;bTa~�������Zo��v���������)$��$Sm0
q��
y���[��i�zA)�XJo,�� �o���5���V/dg���6�T��&�\���k��X�b^��q�F����2����b�#:FIGp�����Un1)g��%/6�^�����G|�������rpj�����[��|�-uw��������T�#=�d��g�oY�I��'7|�'�0(h��u�G�Gj,N��a^�L����,8S�G�R*jL�
�P8�����Bb���%�P�����Fg����*ta+;J2�@���E��L)�J���r9�W5H�U��Q�����3-p�iN�#���I&����T\-�5�D=�$g)�
�Pcz�tI�T'2km9�����h��]V�����?=C�Q����K���%U wI��V���Vf�i��\E�����_7�=[.H\"01D������+E����J������6(:X	;���\��P�B���k����`SBD�����O\P?�E�Xj���&���~���l�W�t���������f�f0�x����5/o��`*],�#i���YSc���)&+���h���X��m!��uV`k�@�+�O�>s�	��8���
�B��]�� �g�+��'��������!�WD�`�gU��a��x��2;-�J�b���-���m���������9�m
����M�����<�t��G�E�`������Y@uHf�q.k����qx|�	��e�i��b�;
����b0J�w������:���}PG$��_��HT��I�2��|[��yE�,��|�)��X#&[�n������k��N���'m��(���������D�#��N'���D�������*�����\�QyI�������2�f��]Y�����I��u'��
��E�����^�4������g��������q~r�Z\��Q�^��}�B�e:(��X���g������V2V2��
�|�G[�a�$����PJE����(��<��@1��0�y�L�)��x0"W�q�H�/M%�1��I�).�o6�M>
���6X'���~��D�t�^���
"��kT;��������|������/�=�D�KbU�G��!����|y�|����!��YB�����?	���Z��%������z1��]�lP����X��U�Z��
�"������dk|��Q1D9���L�~���'����w��&��&&��<������"�X��^U!�4���J���:|Ek��L������hC��B����*�H�Ja����wp�����f%����Gn���q7����6X��G-������VS���������&fsL��l�:!�P�k��xY��#_%f;����T�s�t��\���`���d���Y=D6�MA��mr�NW����B>��D@��DBc�1D�%E�1�v!T�H���F��~�����FF�4	i��z��T1V�d�$b�7�����i�zvy�yL6|p��hO{�I����S��i0I��+L��?Y�}�v�k��B^~��gn�X~~���_��W���NM,s[�$.�V������fM�����p�����s4������u�����OV�2�2��`]���X�L��=j�m/�4f�����������D���7���_�!*O�6��a7��<}I�U�����[� B0m��xxj�@E"������4dh8�kX�F�|�6AE1��,����ICw��B�������S�����
�'6B�����)������RVng�b�T�sz�����g��]tu���s�
n!,�S�����\OA��N����sdbv;#����,T��#K����%�~Q/���skA�_�pL2(%�S[�-P��6���2����*s,���*`0���?����]e��h6������!�AP�L�"���b����
u�l]m��C?Z���X�!�r�Z�w����F��$�� e4��j��Y������k�����c���~9���V���H��vf�:"ah�
D~vl�+1E��5��"r�n�&n����}�[��J5*[����9l���1��JN�`�%�5�����v�L���
�?���xS��Bj;�%.�����*{J�p�|D��O0���|���tG���^��yds6�����y�f�gp��B�baY�e�bF������U���W�5{��t��v���f'Z��C#j��@�b���s�m�z;%�V:�HsI,��<���rF�����R�Cm�1e�;&����l�j[���/�(��+U)�8����a�c%��]0�������Cp�Y�1�=������`2_%z|z4����� *�T_>��#U"�c��(-?�6<}1�d�Z��.�\,��z�/����
<T�B����7�R�����hE����J�����:9�v�5����W������b�D�h�i�����H��O,U�����t�^e"�wOv�����.�f@�:#r��H�K��J�C�������y��f\�	0��)�'�yP�&�`6�Y����4vp�N\.�?��b�6�E2�FL�]x����@Z��K�Y���s=�;������g�B
���/��q�k����8�e��$��|�%zZg�}V%��:�q���k�N���L�����d�vE
P��Q7�SG�.�����`�����lp���>�_\��&�"�����[v>�����pMH	��3s���}f�����H�IiT���^��05�^�����fP�G[p<(#�$���
)�]�Zi�]�g����.\�]H�F�['�uS�8Ou��%:�S>YrA�l�r!jR�F��7��-��x��B%�{8��������|������������O��r���8�(?����p�=�/i����z�u���L�>�T�e�F�Gp�od��8�4��G:HE�TL�pr��c2-��
������J>��N������~��U�����d?���"�+O���H@�'/&�C�37b�R��y�N�r���7���-G����s�����L�>#I�4����[GnM9n��������Jz�*y,}0�j�O��0%9Eg�1����`%�>9��[,�O�
k^�jF.cz2b?/BGK�o�
�����V�A�Z���[�)[[����������OA��#u���W���s�����X�D9M}�G��WvQ4�o
�#r��]���
3	p����!�t����jo�`Uz���������B�����=_�A�OI%����]�F�1�aB���)5X}������XR<��R\�c]T�SweXO@n��h�:��/5�rB�dK ~)c8��3�d8��L��D|�e�&�K��DSn7�*�)��cnF,�4����}d��d�
5nV�
	�;0�����{�W�7���gA�`8l��{�3\M�w�9���9#6�hw(;�~�q�>L�.�:�C�z�5R���Z�Syp�~&��{"����z�R��� ���)�b����H��<6{�>��:�4�5���A�V�
�u0)�4�z�x[�(u���FW�)A$wd5�5����*[�{����PK
kd&kn-�Pj�k&��H�:9�������e>3�5�(�V&3�wU1�ty&?�?���x|�4�4��1D��	�2{�.�o�����O.���hV#V���C)��F:�p��GZ2\\�j5	�~8�<���#�4�����8
?>�X�G#K��G���
2�X��8�
G�C�M5�Y�,V�?8B�����Vm8b� [�KKC�?���>�z���i��92T"�,���
B�
��jqK�!�������_��U8���D5�)�SA��R�H�C�����p}C,V`EF�ut.k!U��n����P4'�6q\����S�?r���2d3��.�g�8�����#?�5�U�������*Cy�L�]1a��,�^����#J�-��b6aN�kh�v"��������6-������9h��h�%6����~|�T�(V�������GO)i$��1C�I��������hI���h�q@a��L��E�Gt���|J�P$Oc����3yF�s62�
;��9Dw�E81E�N�yV�
��p�a���`�6�`�r�
F����wiR�b�h!)a�[zZ���1:��5�T������"*��V����G�7�������i6C�(�f�0�l8���h�G&�t;��k���W������$�l��z)Hu��>8�#?������M����(T ���d�,D�5a�{KR�w�G�����P�F�C�2|�]�&�7.���?�c����S�=.F<����O�J`�>�e4|W��r	������"�D(2d��K�^���j�F��nI�f��%W��
I�
�6��O���tS����&�O���(�Ilz��gmKI�����f%�R�x(*��
q������������@N~�|3:��N����OZ���p.�H�H^8'$������ �|�i��6 ��(���z�E��ML73N�Y��_]�> ��l	�m�+h���{���3�]xq98=n�d�������:�0�����/��F�����������hg�6���6K%��k��#��U����} ��&`�Z�u���(��R�H��VA�?�M���dtuo������H=�v�����#l]T�8���Q�"���Nl�y��&re��!�T��o���	-�����[n�fat����_0_l�o<���J����zZ�v�+�Bb=���SIXW=Bu�y;�(
�s?1��q������2����X�"[������O���"AZ|yi`���/������7w}2n5���x���~���mv�qu�����lk��-��<�&q��RJ�?Z�Bq�|K�M��:��0�32����+\`^�L0����O�lE�������Q�3�d������� a� P1�wr>�	���c|�\�RI#Ny�1{C�X�����+���q��3�*�<�E����&�����b�g��
��aPl�:"�;��k�L; J 9as�m�.o�r��M!��:XW�{�@��:����h(��X��Kw���zW<��2�X�%�sJ���L��h��w���b>�y.xi��
#�
#W�#\1�����H:��O����HY%~�E�)��@����g�g�&3&��'����1����1�	���@4ly�Bq���.�^C�5���4��+9�Gq��l�@\V�b(��H�HB��S�F�.��zr�ku��TG�z]au���$a�x���d�=�_J������^��<~{J5S
�]JP��&G����k{�����e��fK���	�J�(���b��|(�����7������g{e����t�%J���*���{�w(�5��U�����)��3��t?!���
����Qj� �����wL�{X�q���*L�zq���;�O�>.�%��X@����!\_7	�r��ej0V��tg�v~�$�F��j���PX��3t��u,jMQ�-'��
�L �ez�d+�xN��������9Z��k��a�^��ve�/:p������-�2�)8'#�8��-����9������*��:�fV%��Ei_$Ql�b�Z&���/�
U��2��	�Z�,n�c�lK�����q?�R(6���Q�t����^�������l�V:�4z������:�f����dB�WPkL.A����k�*nj*U$�@T�/�
�a���������s<�.W���8v4�]P�F7`o���ms�Z)
0� ���
�8�"/m��:m�[�����`�������
C_����y��^���;�^\���O/9x�Ac>>$$�a�/.Yi�$a�����'|*o�;i���Z�����>�m�wKs=�_E9�TB^�a�������{�{y�������:M�>���/��OC�*Q��U�Du�#�?���*��G��c���
�B������&���t(�,�1~��`�'P}���*UDG<]@���t{�������V (o��-�r�jR�j�%\k�}"{h��
��9K~SXBy���a���������)v�-����RzgIbX �$�*��}{�|���$������k��N+��z
����;2��VI�m�M�+�����f\� fzf�w�=�%1�D
����>����� rii����N5O��U�6De�|9���n����K�H���������m��p��?\;��<�I��-�5y�(�i����Z�����/
��v��������$�Fnf�W�Z{�N��G����<�%�v���d�O��9�����}��k��b����}
H��!��� ����+��c�W����(�Wd�����:2�\��O���A}|u0.���%O�%�a\|���F���8]>h�^^]����I*o��4��Z�yu�/Q�����i���dE�r�$%��AQ���(�gTz
�������+���,���W�o������~M����'(t��t��U'���?��X��[�����k.�X"�A2��h�E�Glq��h���������gK#Qn����D��%\���$�o�aM���*��X���������|�Sp���L��F0	�oBvU�oX���������y�:��vk�� �����V�EaU�Lo�v��>T���*�DVU�8ze������i���{� ��T���;�U^},��Sf.mR1��i�a����juy~����<i���0�|����xgm�����R\0w��*�"L������.��Wa��e�x*�����
!�7o�!��;���
r����t�����E�#-=�������<H��-���@���k�09$J0�������_+�s��FK���)��>�����~K\�����RZ6�u�o6jt��PGZ���`x�0�	�:5�F���o`��M�/R���>vC���q��#�L����(-���$`���
���PB�����9!%������f�o������/;�j�^��P	7�HL
,Q*�k�4vQC��T?7R=�#����p��hse��|
��V���}H�$��b��uWk����������j�X������)"]H%[+F,Ux9�H�+B6�:����4������z��X�H��&��*�;�����6��B
�����,0��b�(;��h,�wP�D/�B��mt)�r��P����r�	��v0��f�����/��A���+��~18w���^-V�H���*"<��*����]�{���tc�$�C�/4l���'�i�q0m���e#��P �$p�|ky����$�����+g_�(��I����,��7;����"�bC��y�[9�y��}2�?�YF�������H���R��36���3�|�y�_�p�U'���e�+��o��D���Z���_(G�6s�n���Fs���a�Tj��d"�0)�*�4F��<��Z�;���j��xR2,�?�&�n���Q7��C����D#q}�8}o��M�X��f$
d����$*�Dnv	y��K����Ps����F���L��8���r��K|I�W�g�Q!����@�(m�^48����a�h����m7�Y0�)��RbY��+��.�@a$A�a�M���H�49���`N�G�p�f�Q\�ZX!�Q��Wc�����N���������u�&"�O�'��\^���>�*l�x����v������`)�Z����'.���S��s����������u4��6z�q��,�C3m�D;����4LjN�}3����-��j�0�A
z`����%'��U��8�+���h��X�q.�5���-�)|�S0�y��w���T|R���kZ���������t:�t3���7,�m�`�a���Y'�l�H	oVK/f�v������1��.2����,�|3�s���B�(���*X����E��+��uQ%��03�����<�_2��;.]�6��H]�k������9;�K�2���R�:o��4n��k�����w�����RL��>7����e#�tQ��t�Q�~�!n�9�)�/:����!h�e9+���#!Cu�al��-�`��
�R�]�����NQ���� �X��� �Pb�F��H�h������N��KE,(����1�m����x��
�[���^��7����Qf�wb��d�~5��7�_�%Z=�]h��{��o4e��KM��y~l�XA�(�$�-����r<���?�������J>�#_���_����[4�}���#�������+l�%�W���d(�$f�$��)�f�F�8�f��
�z�M{�+@d+���Q��Bk49��Q�@�ix�Y�E��4W=�+�>0S�����&O��}�N�NN��0;]�U+����<f�|�/������j�A�#	[�U���<��� ��3��1S��;��<����5��Z ��6b|x��x*�������Y�p��c�&��xl��������M���Ay��de��^~�6��GT����CH�@�r������EO
F>w���\|������t�����������\���������N���������	�G��N���������w��UxV�-��A�Pt�1@[QD����p+�"���VO��>='�
TZ�[�S���4U�}^S�yV��z��=���R����~������'S�eQ��u{��U�Y�������1{b�������h<h�E�G�n���L
�5�<$OTi�Pf)�	�q�T5��q��w��Z���?���}d9?�Z�K%*o'S.N�+��r>����������VA��] I5�m%C���%��.�����2�\���h�h��~���G������������B�}(=���S�%���r
� ��NC^@�KWs�����B���E?�[���G���������i���}C�R`������������cv�
o���-��ef�����S5���R����2OUo�_�����GQx=8��+�Z�H��C���v�,�3b_��]B��+
f�,��I���,��Whr�9C|���|��Q��b���)���utf��A8DGxX9�	�aG'P!	���(��Vsg>A���D�x��T4Y��l���`��57
�r�q*�"�"F`X�����>
��c=)N��
E3����z��,�Pd>�����]��!�������O�G[�T��;��]P����J������hG".#��?�4f<�F���������r����t�7K��#w���e�0��N�F`/���md:-����`���`�_m����s���(��>����x4�JG_��$�����Py#U�m
Z�5�V��L��z/��)v'��%i����A/*�`7�v��
�������O@(Lt�a8�������9�!��lt-�����0�2`"��OY��FB\X�`:}�r�`i����!�Q�n�������mHQ�:��9�|�N`LS������A�����I���Xd$b�
��F��*�������{6y�����ueq^����~�������?����:���~98�pj��4�&����4���G��C
D !��#�!|P�u
�;V����8�F����M6��L>�59����Z�	���Y8�z1�	�2���n)V=�U~yT����l���jy����>*M�us4{q�H!P�����X�1�r
1��"
��2�G9�~�W��*��I0b��[���2%U����������
�KE��Mc�0bR������`����F���^ H���;O��>�dD�*#��'���Co�����I��j�w	$��dPTQ�������<�z�:[��������r0xWc�e�j�0��:�d�m���&:'I!)>�2�V*�i�N	�Z�:�-=#�D��vg���+K7����\�Y��m��Mb�d�F�lTL�.S,$;��pI��&��e������r.��	�<�`]�P~�0@@��[�K���# 
@�H�����h��d�u��MK����h�OR��V�
��o�s����`Z��b��;�pJ��V�����;2Gu���!~�F*��;�*uG\�%5�`*{���dH}���P0��(�ge�<�����d?�j�)0t�Zd�����I*���\���-7���+|�9����\���!����Q��p�q�!��s���/^Yh|LU>
�~6K;�	���2X	��s�-]�xm2����z���q$��R��!��I�h.������A��0o���@�z����Vl�jw�����h\�	��]����M�\FhQ�=\.{����,���t� D@��aY�<��qO!
>���3�c�(X���
�����.�$,�7��(F�����k��o������U�S;�(�=m,�c�:��g5�5�f��P��n��M�����'(����_�s�*��8�����$�Y�y��]	�r2-N[���.���[m�-mu���(
���9�����G�Eat|����p�B���r4��1��%��
U�B�O�$����(�nu�[q>��rc�#@�xVq#O�GV6z�mY���4�-7�AXX����t�?Q����))a�oe�,��OIA��Hk��P+PZl��]���%����x=���#�B7�S����n���N��p����EW���`l�x~i-����I�}����-cB������#�66��K%��C��������0�K7�R����i�q��~��[/`��3�
)J�A�Fn`������L1��X�w�<\fW��,!��b/7���1����\����@(��p�:��$x�;�A���s��
Ud.A��4{�9N���[��m�Zo�����pT�,���(�������r�q3v��9����%\�9~�nx4���$�V�����t�p-\��FA)~����"�{���0G�����bm�j&�H=9T�������E��x��&n.*b�����wQ�HKx@F)�J%����7���Y���.Q!���
������?Qz	�ch��.�4s���|M1&7s�T��3
QW<f%2���;���C�$��j�@6������7���.�f[�q&@���B(���V�0��)��R��^^�-�e���y2%g.:��f(�
��J�`�%���J�'P^���v���RK%���)�jH)�
��W��8�|���q���mc���o~���~�>�O0�$��F!�h�M�%�L�����j[�R�����������,�L
Y�}����;�r��FT=�YZ]�V���8�w�,G�DS��8��(������&�Z�5�t;���T�=�:hR�?;x�b�S;!�Q�
�����f%A���4>�����<�g(�\s��'{8D��I��a�`���<��C�Wb�9�}?:�����#u!�b������[.^rF�a���Z�7['8*\�h��UJ1�������L��F�����>C1Qb���������D�H2UI$�I��Mb�!y��U���l�>�����&Q��b�.��?�bu�'^��I�7�,t@(^����@����9<&Y@��Ctv"�8��I��w������3�`���~�=�58cn�q�:�U����0���M E#�zO�A�e0F�A��1��A��n
x�
����{?h2��p5����928�Y]��m��=�M�g�'/D'�@�%��D�X���,6�	��%qi�8&��"��!�	b���_�<}�'' ���c���I&>���P�u>��5�U8z�,��}-��C�U'�V���J�4zM�+og~e�#�J|�BH����%���I�T}�\��������+�(���&w���0A�]=&o��.c�/m�E���	W	��0�!�����>��-�a��_��!��3+�\��^����jS����2sVi��U	�#�������R!~��|~��j��LIAx".��0�zO]I�d���>����<�Q�����N��u�H�qA��nM��`�b���2@d���.Q��,&,1���YwA����?p��l�sTj�����3���/�6����!�|R���+i�>���O��N�s�v�F�9X�����*~1�h���j���M	��������g���<�Y��]�n|O�X�vv�Yr_r���f�u9�b����iw�9K������f������B.-J�?-z/�:g3>Vg�'~�n+$U;�O��]I��W#������q��a���cB�N�g�+�y!������h�N�����bp~yq���)�����O����]3���g��_�ob��������p�oHjm�@
�#1Y���BW����dp9x(�����X�M�
�*<H(6OHt�l��?;�?$Zb�%D��@�dp�f@���
9�[A������y��2���������[ w=�N���;�W38}�N�x��=+��*[�b��S��A2�U�L�4�E���px�d3���d��*r�On����v�s��'�S��
��+�u���lp�=��:�c��ht
}��
��
�D��*����g���Gu�tK)��r~g�u�R���RcUX:"��#n��"�DYu
l��0�@�"'���3�*�[l����P�a;�����H�9�����Lmq��w5(Bo��O.����=>�\dB��Z'
���%��g�����F��O�_������MC6>F��M���YW��by��.�Y����\��TO��5�,����:@3(�N�
$H@(�7
6�G/?J����*�%|�M�����A��j�'�6#��|��uA&�.+�q�o�O����!�>	��Z�&��~�~��Mg
��SJP3f�T�T���S�snv���a�:M�%��Ng[ �U��~NR6�0%)���f�
��?��wW.rV�����������[`����6�s�8�J9O���:M\�s���g��R�O2���W�g�uW?��?A&�j���(���UoY��� �W?��FB����t��)���l|>>=��F��6�p�Ym�$
�������$�sv����C7i��2)��c@G���Cd�DgWDB�xP����:_^aa'��-�1NP�Q�v��t�}�.AY<p�cl����.��4��������������Ztrz�^G��mFs�i����6��{0���{��L��,��4(�#��(��_�1�	�`x�/�<nG-,�����,��XG���*T�����L.�:��������xC1��RU�A�������S=>�L%�����#������`��X��o���H�8���
�[�8�X�<��#�9��H��ct�5���c������g�<����,6���8|qv�?}[�>�mPf�����Of�*�8���Om��:�:�y]��+�d����.:�kU��0�A��Ai��7��0����Y>]���Q�����D~�:e-x�X���x1�wGV��"�^���8��z7�r�g���6�h�%�K���[��'���������d_�uf�[��
S����"�e�#H��R������p��YTD��~]��I8��97m+�[�6���`��$��y����;�F{����&C���0�w��/��	I����M��u&�;)]&�eo�|����������
���b�C�����8H���c���w��C&�:�;w>��]��kq��_���([J��m��/�f�.�6�Y'g�v?�D�Y O�x��H���>N5�sN��Un���7�?{���f�N��3E�Z�f��lC���~�Qo���$
��mq�3�Oq�a}�C&R���	��"�+m�C&\;1�����L��)���&�mb�`����`E��L*��$0���2�G?�=��������E+�����xJ�[tM.dbyN�f��n����)��urfR�������!j����)�LY�A�����4NEq0�cI��CkSt�q	b�	}A�����\���7%%��NK�������r��8I��9���8UR$j��������9cA�H/ER,g��y�$&�J@�3H��X�������k���"=XR�[�RtM��rlYZ&Q�/��Ntf<���f?g=����x���|/�q ��e�kt�?>��S-�L��W�F���I��*eN��u��4�����3r<��X2�`=��!�xpI����b=?�i�O�L�P��V����Xos�;&>n'}��O�|��oL�	)��qT��&Y�
F9�*\���R�u��������`����`���c�C��N�Y���*�s�9d<D�������o/cA�(��vU�
�������dx�?�����4�z���Rv�(
�72���bz�M��1�K��2������6\W������e��UX��g�%Q�u�����z��;%7{��%����R�H����i�C�EG�f�AG	��(8n�����WO&*[U�	��s7:���������#\��V�"�dS��8�<�Q�h�Y��h�UE2�k���f��wl�6���������������Y��U.�$��_�.&�hZ5	mD�����r���Mau C�:���IS��$9����hyr��v^d����gK��� �c����x�HW!���f�Z���}2}1���T'�2�n4!�J�}��Lr�Xf1��R�J\f��])��S��A~70A�yNEY#������f)u��J0)z
d���5�6�6��������@#�p�6.*�O0��q�0g����A��+�
���j�
%���/�9V��*FV���-�f�2������.{�=�B����c����5��9D
����g�`~���}-x?/�.�Wcck_�7�0�8=�=Q�D���vIF�U�)�}Z������������`�/����;�����!I���px9�N
w:�����0D�����v������
R���D|�
���Y�;�jp�u��a�O?����w���G_�3l4�L��:'�!��v�%V����m���
:}N�����~ �-�.j�'^�����#��XY���TC���k��O�x���
c��s
�kuY%/�#�^�,�,5����_�o{���YNF��Ye��UL&V�#�\J��^+�y�j���I�rp~&k�NQ�k�I��.��X�Rc�������#����G�c�{78:�_�r
_�vS9
4f���{x.F��W�xq=�L<���^y�<��X��<����Jp���*/����b�$&�j+�d�V���[ ���,[���,{xm�PQ�0D3�1I�	�x��5 �j�n����9������W�������P2%��/�9M�@#$��rk��Nlf��l�a���)�`�R{Q2�����Qxbrfe�M�]'LBD���~b	g�`��������5+�)������)�����i�6�G����gvt1<�|{>|��@HS+vT�]~w|z4�N�������W/�W�m�>&h��X����W�n�V�N����*]�m�N���r�l
�����f~����z����^�e*�F,��,�Lm��5���TqW��,��s��JI$QTI$��=�0�.5l� Qd)8�_\����7�������|��Q����Ze�����7L��`�I�FJw�_��D,���H�oa�x\�[��W�����R�M�V��?#�Z�Hpl.�W�����~�({i�
ij��
����������7�m8�7fi~�)�u;cm��Wc�T8��p��AB�2�0@�6���TB�v����8�J2����������_.���RZ/�f����I6|��`LGa4�W�}!��+Maj=�@��������
���ud��Tk�?��_�#]M��{Q��?�"�;k[��(�?o|S|�:��
���|�I����;�u9�$�M &����3�}@��c�)�NN�D�YZ�����*�gJ}a�0�����'"Lh!��;�%����W��/���3�^,���'t��W�
���6�2Gv���a7�'��p�����y�T]�0Te0>���c�
~w���d�����r��G
y���8!���G3x�)��At� ����&�$����m��YL +���<��=��a��v��i�b��?�j��ONG���a�h ���R���2|;:�������G�t ~�bv����������<���Pp��`�.�����@P!�"�~�����gxr�5����=�{=vc�?/���KAF���?=J�C��_.a�������{zy��g����N��Wf��_/��S�8�P���O��Wv��?��c+�f���`s���C�
t���=J���f�_��_�g��a �k2����IpM�4Njb����de���-��e�kUP��j��)���q���0�0?$�
]d�������>��'���Bh������8f�nj��s����s�]�F��|��2�)>�v��������%�2�~�����r
���8.d��O�LQ�xZ��V�q�� ����
���Yv���:�5����x5��c�C
QJ���b����65G�=
�>8|/�:;���<�`�=�<���>�z��|��N%7�
����BZ6�m��1&�������]����k|�Q%�{O~��_!����W�z��7�0����K��{	SJ�(�
��$r(M�<�zZ��Wgwwk@��>�2BK:P���?=������>zs|>8Q���������i�u�	����p���X�������_n
��n�s��y��g�g9�0�+hf0�� ��[i#������p�gF�$���!��w��:��z��HTI�	�0�Q�O��R������������I��	�-���*�����n�*���-�g�3����YN�.���"�W�]�����-zj=��f%�u�S�������R�9b|+�D��=��t���f��I�K�m.��<=8��e���98[���=���Z�Bg���@p��sWKY�l��sWKU��C�����������@���)*�U
�ut���?�>�;]�@���wg'��e��YO��>�����?��aE�6��[�Q�{(�J�cQ�����8M����%��T�&/�2WC�����������A��}�be�����5t;*s��X�sm�3_g��sue����������K��>p����-�c�q�����K\-��k�:k����YEv+zVi�Umt���u;�M�����n�����qm���&~O��v������"U���� ����>�v���eZv�j�K]�� �F���M<�F)�� �M'T�,2���E%}�	GI���M"}�)D���u!&���[���E����)}w�?;3��D%Y�y�������1Km�����Y���N^k$AN4�������;>�>�*w���XE��Zw|l��;�������'B���tQL���a3]������\g�'K"�"C$�|�/���Dt5��U��&�7'.�����	����}���}�D	�|w
d<K"�c�����������*���@����V(�\�_�
�tmKJ��c�
�)�n*��DHz��G�w��
v�P��t���r�K'`���[��(�A�f@"��;��_��ty����N�����Cu����
�������oT;wU��|������7j_8�;�n�G���h���y�6M�L��]Y����m4���Z�jc:���7���`�������T�cV�r�i+�4Z�%�v�_����Q�
n��^H�u���+�zI���/����r���b&FEr)O%����$0�\��H_P����LF(������?����jR�/�/7�p��!"�B6��\4���Zm���w�&�F���I��X�A�!���a���J3x����8�pL�����8��V��R��3�����+�@9�ZT{�^���L�v��.��X�x��~��J�g�}4��7���UH~	�Wq���r_�6��<�N�w�K�j1��3�IzO�������qt#�A�eW�@\�{�����F����=�;��2������a�z�(!�L�rx\����@���1Th�e��.���K�
s������:�U@�V:Gw!��*`�v�_C��_�9*�-+�av�*��1��,JD3�(~�����V��
� :Q�����v�E�G�M��x\7w�<;h�:Jb�eV$��8XiQ��b�e���J][e���W�C�YE����P"'76���J�}�.K"�.1��6�r��6kq�\D�ozw3���8�m9��jcE��H��
nh|K"N��,^�B
;�gj��b�8���J`�c��]��I�<�g!��<
������V�Dk�v�X)���:�\��y��&��x>$�f�p� &��(wX��]@�!�2g.�I�/������|�k0�/Ki�"�K� )��G���������0���P���}x�a�����kx8�����e����?��.��\����S.�>���]�!������%�o�W���X����P���G�x_�,f��
^���B�y�"�L�w��x�C��7_x�`%�H��)��$������c^��������m�#\p�.c\�\���w��#�d������^qJ��N�����%��"���
]�����F����[��Dp������b���J�,j�9
"�^� �)b&�/U$�������k����c���"
�p�5x_�X�^�h���?y�{�~�?��pP'�����-}�:��pq��y���Y�LL��i���iB��$�����T{,�~I��N�����oq��R��bp�j��������xH������3'H� F!K��O��s������=��o�J�E���Q1���=;���M2m��.��DTm*av�h�A��M�
�Kk�v��T��O5R|;aaT`�@�*$�������������z��_�o�����[�6�h���SY�b��U/0�Z<hk���:�S����!��|3��<1��}$�'�>��X�X�*m�~o�8i�Q������&I���)�U=���-L
.����Dc���TfU��h���~����Fk"�4���v����~WRA�������x��%�������������2Xl��t�'���*�����k8�5�gv	�!S�mx������f}F&��(\�����f<����rCU���F�?�<����X�	�5�o��nO0�f�U���e����@> ~z8����K�Icd�p5M�cQS�mH"���BF&5Q:ilJD�CK��l�K��Bt� a�c����F�� c��Y4�$1#'k<]�6)BK3h��)�:�qB�:��P�2h�(w�1�OvM�f�O�
xG��xSU 1�����9h�������7�������<����F�8���,N�C�4C�����X�
.�-6���+���t�����Z����x����2��,�EL��I�����:uX�������Vr{���WM�b�T�,!p"��k9[Z�P<f!So��\/����A���A�Wz����9 ������+?��
K�A;>l�-��b-%�X�t��b5��x
�����nZb%�����$�[��\��S�J��t(��J/�x��e(9&����
�b�����������]���IMq���=XgW2U|&��dQ�;���� 7zb�$>�+	����)XE��*��o9�����Y�^f�Fc�<������������A��&�o�D!�	H��f���dv�V�M���h����F����(�R�Zm�[w��o���U��8Sp�7�H?vvp�Tcv_m��$��x�k%f���������kF<Y��^5��)�s����+�d��i�����6Lz���S�O��J�ZRH�B���NS����F����M�u����!<�Z�+46������}����c�����Mf������58R���'�k����r^�I7��A�����S����>`��:�0�!Fc�[�x���S��fw��q�Z{���m�-����Z�!N����ZsXc�%n6����<N�����v�(��v/r5��2h�}��w���X�]� �}� ��B��G�&Xs�F�zm=�KH��3
����f��+?���~z|�6BKo~��(U2$A���:�P��'{�r[H^#�/������o���'��������5�8"���"�/�G��x���c�8t����i��N���bW���|N�x	�!4>�-8b���Xw*�b�a�?�����S�)��w�D�������>�����:����K�?��4�:�H��'���S����Ag�Ow�q����q�C�7T�Z�j�L(<����o�B��[�2Z������k��c'5����;�p����n�J��p)��GV9��{h*�7EF����Y�,V�
 �Z��~%���� &�����T�F�^�����:��V���)�IH�@����F�����
V��*���������S���txz���tW���P��Z�
�	�O��/�#��%K�5H�|^���#��1���BC��L'N��'"�C2���v���	
�iH+2�<���)y�x��_�vu��Zx.^N��/!3������~�� 4����#���OE��Kf1�)`�y<��Z�8"������T���M8�.���n��]���u_4��F�����-���w��b	��a`����U�<��������<D���=|9l��0��/��
N/��SzB^X6�������u�	�hJI3�������j%�[�x���h�f9�W����9Pl-�Sq��W!&KF^��L��#��	0���	v,��U�=>�f������U��#+p��7��������bek���s�yHP��*�����3Dm�� mIf����[5�(>��� �l�r��I���	���HR�Jq*���t
�a��o!�5Q�A:�[t!
p���/����"��BquF��7D`/������6�zw���#�� ��@���1<>�8�T����`
29��H��E�pM��5�����`�(�������<)3�(.�_`�g���
�;�*�5
f��$�0����|�k������u?��1��#M�X`�=@R@`�i�^A��0����E�S��`}s�����sk8�O���h)~ym�@����]<�;�..WT�X���(2J~�%�"���*�d�%��2��W�(�U�V�W�!�W���W
���z1�O�&���b��>hF��9��'ZP-��X�DA �#����+I$�
��� <�(r���\E ������H����\F������GCE�@(-H%+�I?_6d2���X����b��N��6q"�<�R��Ek���@3�����x�?���O=�_F�����$<�u(
��i�n�=�
Sq�3{���M��D|��%>��F����6��W�r�����`<�(�H����5Z��f�]�0|����T�0��Y
dV��g	|�#���_@�OG�+�u�_�Kd7���)���������.����Re�j��7R�_�M����^9����.b����Il	J��sT�#��jh
�O��mt]n�
�(�K�!T���)�H��|fm0pI��J�%�[_a$��+��9R����-���p�OV�,��;qvw�>�Op���z�"+&��sP*�S��!]�Y��Z�A�n`�
]�]�g-�-�����"�Q7�?����p��|1q1�\�3��L T6�2 ��zA��	�Cne�P���3�8��W��'���������j��[iu��T���T�����?��dAG�P��M�"K��of�4�p1�)�HFhW!=�s�� �0{������Q�%���0@	!�fVH�*���Fz��	�] �e�X�^�Ri@�#�T &�Q����z ����I��S_@8�����.$�*�b��vN��8�������\�;��]d�|�#������0u������P^����#)�|���}�/=1���������\K��U�<���G�H��06��Q��#��c�Mk({t�PJ��,�Co
b�^@8���,cpF7G����D������3��O�GfC+Ax�s�1��=�`��\�
����O"����?��iP�#	�m
�O��A����<�����������^1G���o�m�^��
����������6D��pT��[n`������"|�a��[��p<����^s��mp{%�
~�"3���g��/���cr�A���	1�"��h�%iU�9����M�F��bj0�$�#KxH\����du��6WQ8	A����z���b���H*�����
��x��`�N��E>hP�R�*A��B!���0r\��A���+0QO���T5�n	�\�%�yB�1;�?���E�O+��� ,9��c���
(=����[���+��d�Z�1}y���*%}�(���2�� 2wG=�*�`��R���h������h�~��neJ�d�j���z/H��r��s>�(e��a0����$c���*RH�SO�o`�F$ }�����i��iyP�=1{��FH�Y���f:
����k�6�v\4��	�^��7NJPHK"�>��E������$"	IW4��Kx]\FN�Q��,)���<���1����'[���6���6����T4+��/�6��c��+v�DVr��^���C�SM�W��S�������G#�����7�{vm��Br �
���(&v�;z��m;�
�N~���_��m��U� �z�1�0wc
���A�����R��3�N}'�����2��s
�����Y����*��l��Y��,�
�?~�L�!�{?��^~��'�4�����O��,���T�*��d=uL��'�cR}4)��N�O��G�H����?���.�����r�yl���RgR�%�m��g��#`9H�v��]��&�����?��t�i��s;T�$�DQ���pf?b�cc����Es�V	Qxb��b�]�~}��.A��63��/� ��*9f���W���\��b��w���F��N^`�@6
�"R������������g��t���p�C���5��/�c��/��1��j���N�R�oxn6�E)��G���G8�+E��0E��
�����;�F5�h��E��Yk����7>�������,b�k��/W/��z�[>&�t�}����I�X�@#1- ������>�tC0d��W��Lp}������P�cj[]������n��{	����:�����-�f��cX��Q0%���|����/���k�V�knF#,�a
_D���d�~����?���vO^Gt���������+��O�eY��(�cF�t3����QG��Y�X��o�4F�����F,~��1�.���{Q��7��8c��o�7q���4���1S]���9������X�8A���L����m��36���
urp��s/�u9���w4x��������4Z����9c�3�C�r9Ld}�k��r�P(T�a��a s���]�/�ES�
M�r#h���oo�?N�q4�//��J��������l�"���5���)+�YM\�'������"�_T������`:��*$N�d��<�>5�����Act����T�*�������Ek��-�@���b��D���L����.��{xE�N-*�������nbI����&&��o���F����qd�d�|������w��A��$x	w^f����h6�����
	0�������hh������/Y���D���,E;��PS0��
5��p�5����B��X��l'8��*��,	*tG�%f)	�e����c!I������F�I?d�2�@1�/}0�|�� �gcz%��P�H`�]�r�S���>���2�Jus��,�n���gp����R����<!|��	1.�X��*|�ZU���'D�����zz��7F
�=OBt��Z��
#��pR�����>�>�*��7�/�	���s�"Vi`1+���v��2�4s���1��*n��1�������Vf�f��\����?�t��E�0��b���3�s�1b����)C���#���yp7�1���q�UAA�!  �b�GUj
�����3~��� H!��ZR|	DK�2`�&���2hSA�P��I�A���nk|i/��@B�[mH�����LRD�*�c��G#�B*�U�?�"���=F^�b�*�W�6]ZbE���EKYtu�Ip���8��B�+%Q�o�2�BT?�u=b�����F��I��<�sW��6�O���E��
��U,�}��g;�(�����>������K&�N�l �mv8K������|v4#��x.
�m�������\�����k���8q8U�����-���u{�,�s�zu�J���_�w$w��+��k85����+�6��-���[��F��U@�B!��s��tW�$���nZ7���C"5N�`�����*$�(K�������'o��|
?
�N^A����`=������,�4Z,#Kj$�1Q_�G����(ct�\�9k1���|W^{$8]q��&����;���S�#�O����^^
�4���p���Vs���lO�@ID}yo�c�����H0y�K��k24�=�%����C(��L&.�~�������|q77�|���&��0�&!Z��p��<\���=�Z7��~3���tD�c�i�
��F��-�xU���G�`��<���$���P�-X����1��������O��������~�h�-`ADcX��|�������Va������9	j,&����&����
I�}�AS���;@���tL��� ��Q��}f�w����%���������\�����FzQ�R�������]��Pi>��a����(M�,���6W�kt9�rzI��J����+����cz�@��5x����k%���J��@���T&��O`�������0���X����E&|�+�N����c]�[�Z�L���*�J �m8��C�	�l�J�D5 ���O��Sy�z/�?�l$b{���U%�@r����=O�hb�q ���v_|���U�t�S#�����U
'������y�L�� 7���e}�S��>JP�-��������Q����+WV�&��-�<x��~W.����P��)��6�3���M`[��x-���)����R�MP���x"����|��E'�Y�S_�����$��Un��)��U��TpD�y�����F�`�rH��[�`��L����*No�
���anE=��P�����@O���t{�7w�q^�M���
>����J"N���a���u�C��k�]�R!��]O�Q�Rr��No��H�;y��a	���B4�_K�`!AI-�}��/�oU9�������8�`���7�`AjqT"�$��&MGyo}��_�vC����*��
�������M������}��}�����;��t����_6���=�#�#��
i������������X���-����\4]H�$I���@����J�����z��,���E��Ir�1q�C�\1
�@����(����K%����n��������I�����s��H�����z�xM���.�&�,E��85�8����ZR��E����?����� /�P�s��Xq�%��;�W���+16�����	����Jx���6�����*���%�J!�*��bC\�I�Q�
��9&�MJ�R���WKAQR����1�TQo���J�[pJ����
T�8�2&���D_?@'l�]I
�n2�U���6��y"zlf�p	qDCq\1�
<��o����I1��<�����l&&���|�Fsn�:��������u����T<>��'@!rE�w�JbJ�e�y�����be>�uM����2��X���������#�d���y��_�x]������Sw���X�^9�����i�5�3���B����r�
�X���z�:�-X�"e��~���p:��k���*o��@�W��z�*�K0x)�U�Skn��3�����-�����w��WQ�x��dR����r��yg�:����c�b���4��������"���MIH�(��m�Wl����j97���~���W����
�C�WN�>��5�����������9L|A��K����	O�E6��n�F��������"�gC�T��:vm�=S-i�#�v�����Iz��!��D��f��GU���S�o���~|���������]}B�?�s����Yn�&zrEJ~%�����<�����6v�NKE�/f����8~8�SSE@)|�9Q!X�cD�)�=8�,��xZh��cL=�+���x��z��MM;�8/yb�Ue�{����5M�D��� f$��/��j�A\���I�n$�Fk����U�Y�[��CD��T^o0j0�w"��@�3W�����8�~�kyE�28fs1����[��.vv ��z�'��i��G�~��~���Z�D���5/��g��~�����?���%��~`�Cc���XE���]��C�%6Q��1K'����`U�`��{�p���@��Sz3E�?�|����^lWj�4���4t$DtP|�c���0�����R��	qq�w�hW����]��'�d���Aq�	�-�(�-���2�4Q@	N�Yd�A�(m���� 	s+��]h��WLz��$)��`�@��T*OhQ��?� �[���U�Z��"�H�Cb��2P��mpG��d����E�,�^[~S��u������y��I�����(�&����0��x�Hl���@�L��"�xx��1�G�F��T�i��r����C�`�9�!(�WkU�<����~n�Q����ER�DpV�J�4�G��*�2��m_|��M�������OUnb�v���	x�9@F	PL�������8�g�
?C������x�|)�9�#P�����\��k�$��>����[�����z{p�f��kH�@��8>r���1�.�[d�����X�g�A����&�q��mf���sO�l� G~�$�U��g-��p�!�db��$-�\qf�
���C�s�\���8��j��U��j:R�8[��l�rH���D'{�����2�pQ��"�������|J�R�7��U���.�d|��g�����tG�*	�[��O&~i�0�U��T9Utp��Va�[���Y@	=c!�^^?I��Pf����Z���������'�y��Q�f2/]�)����^b�:�]|���W�N�<\��OmI���j��>	�Y��n
{�i�Z���g�����7CH��A���ET;��n��b�:p�n�N�xS`k���H�\l?P�i����]�6�0U	U��7�_�_n��IY�NR�-]Bl���U�?�'p�BZ���!)�2��F���>���dH+��i������9b����2R\�;��{�����c/1��4'�#
 ���b�K�$@>E������@!�N�]��o�\���Wo����8-�eC��bp28�����!����$\<�~�,g��m
`��������;���������S�a�%`��=��s�>�f'��������4U
��;�����k0����_f��z���_-�zgo����g'���k�,n�����
�w�3���� �����c--k!�_�<��*.���N?�fJo>���Y��2G�ZF�:G%.1*����QJ���n8�r�Q}�>�il
g���P������z/����^3R���#Ne��U7)�E)g����24�P���2�_����F�`����$�����=9Q�)��Q�yjK��l�q*x���gZx��Uh,L_��5���7cp�5��$sX�'H�m��o.)���|*�<�������P�������O�}�n�'�E����w��9�D�p���[��<*�f�#����e��!���(#���EsO���'e��1��c���8TJX�������|z������N����"//��&e>�j�-�X�-}:�d�_�_v9���S��s&�+�r��0��3�'�����FO���
��.�����VP�
/��a��_y�d]�YUE�>�jIT�9>����v��&��G�8����.6����+���-��o���\����7T�y�9�yH�A�������7�z���b�����c�KR�EY���L��\hvS��)�������{vz����<;����c�}�d�%�B;Y�N��(���"O�b��s����TR�u�a�<y~v��`z
3E��GQb�hT�C$T�h8T�!
�d�����U�B�l������������u���d�\7�L[����L/�.�[���z	�<�j�A�0���R����E������R"��w�x�Cl�b���Q�
���A{?�����qPo$��o��m�q~�}�mL�@<�e*.yt6�
X��/O�_�26��A������ZV�o����8��S�1�)�
��h4��Qf�N�V�Qmt�i�6k}i�������	��c�:&k�w���������������#�6�>���b}����c��ky�16���c[���j;�5
2�B7�����u7%o�g�-Fp�*�v����@����$s�z���u������JtB�T2#���=�mu���Lm�"0�����P���g!���:��0��SyW4�f�����s���|��\�:�o� �������wg���'+BaF������
Lk��nc\��j��^o���g��E&���Az@t��l��/(,fM�V�B��?��}.�������/��������@����H� ���;M�PQ.�}������]����k�*��{Hn{i+�w�+o�rV�t,�KZ�Koe-����V=�|�4���k4��F�vA4^�WA�������W�)���>�94�9��v���}��B�P����$�����T��o�h���i��SR��9%�=L�o/1�J�)of���[�����7{�"$\uu7h�$(�e���i���I���o�	�I��q�:H!9v����E�������]B�x�u,q��z�
jO�w�Q�4C�U��D7�;�����������'����ff)g������UtB�������b�r4���g��T�1�4�Hj�u����'W��%��
��A���7'p�F�O�>~w�l�{
����o(Nsn�M/�%2$QY�HV=�����n\u�\6�:��(ln������-����:���v�S����/h�m����y����it��*���7?wI�������D�����r���/�O;
������~�kC���7��Lp�z���*�����y�6�x��y����b�P?����%�~�&�Z�'*=����`V6��	��QX
�0����3���EZ������j)k��-����~,(���
�XM
����������'S.�=K^�C��3]��#;&�$�bv�,)\�^�����~���\�S�j���j��=d��)�����#����������q�9A
��3*� �O�����	q,Y-
��>�!�q�����������y��f����C��T�@��Z��F$�:�Q�\G���W������6�gp�
�@*Y�k2�<Ek5�#q�0��(	��f��x���$�1H
i�Q���P%b���7���g��i8��b���%����qr�j)���p���\r7�W��G5�������:��q�W��KH�b�p�u�p������s�e��~��=��8���Q����(����K����Y����-21E��2v�&�-��}�]��z�wj��\���i�.s;�s	����T���z3?�y��������K�}��)?O��X�����~�q����O����
��D"�L�I�����0�.�G"����?\��8�_���'��������\�����Z?+����^��9{q%8�������J����a��T����l�@�X���K��w�I-g��@`���GI`8�����:d�"lYU$�;�c�����w�$�gv�Bf5<�.���t*L��G�C0����/.F�/��w������U��r�n�%��\Um�TYBc��Z��^��^U�9jQ���D�4�'��]��#�.eC��_\z_<���DC����*;�HO�����|�u���)�1h�����6]�o�Q	�f-l��k
�����z�������h�����h��F���F�I���7��b���&�q����������ZCd�y}��O�Q"l��
�x�aWp����7��"��Lr>�vw_���sJg�-�����$� ����7�!5�X��,4kl��A�4c�sL�t7y��E�5������X��f�*�������/�������4Z�,w`�"S4��hfO/��c-:Rwj���+���'��?�8��Y�?��n��x�"�/��(������Q�����[��}�A�n�Rp��������C+�/�
��!;qj��T������v��b�����BO�,Ef��#��U����U����(7&���=�h�����(��e���R~���hofO��3@�T�9���y�������;H1@3�e��C�j��~�`��'���/���a����j]`Xu������FbB��G%�#��!Y��T�b>U��=���rO�[S�,����gm~��i��~������ �=�{������f�U���x,t_4�
q1�-"�7/�9���W����c0[,����j�Y����z���J�-��~"���Pt��,�.V��W�J�8~�`x9��>����R���� )yD^��IT
�
��j�2�6mQ1�t���.��QYA�QQ�%)E�4�'���A�����&[���5:w�F���x����uH!"�I�L����GVW�#i�K�����|kwa���'��6�W����@�Pn-�f��r�gZt���l�#�Ab��N'F��������M��
=AI�0#wo[�`#���D�q���,��a�;��r���;�T�D�u�Qu�
(b �����O�����m���o-+	�pJ�J��0^�������<�'I�jYx!�={�yr��+�P���1�C�.]OO!��=0�CGW����(���i���=6��6�z��zJQ��W�q��b\��ur�����K9�Uy� ��
R _�y���
�e"�1XP��5�R�������U���>����i�m��l�Gk)3�`w��f��+��=F%j>��������
$;X%a��`B����:�|�]E�D �I&$G�x�k����[���[m�_��|�U�`���3$�6#f�	�918l4�g�bV=i���6:�Zm���Q��������uIXSm�&~���e3zs{��
<���Xka����4�fj���)h0��}q'�
�&Hi��_��x1ATK��PLPlo~,���e[e������q��)l���a�~�/��2�`.�:RL�O:������NgR�^2q��G�&4�'�F�����9<G��|G�z�:^��������$�����*�$:0X.R�'�>�6��;sf��,x�*>���.X]�������0����g_��EQ
�=���*�P�z�����j�q��s�Cev��O��3='%���
�t���
���i-���Ily�/#�%t���"Nc4N�"n,g����I	�E
�J��U_C
k�R<�/y���v�����Y\ME*��}�#ZGC�Xu�%3�{���.AG�X�=����j�����3P/����U2P�r��I�m�C9D����0r���������1�ic <��0gRP(����PY��%�P��9�_���/$��v�T�oNx��A������!��+?HcL�p�Z!�M���'c�����j��h�_cX��l$�SV2�<
��S�0���v]�������$����&�.tHC��������6�|y�sC���~2i�C�w��hk�����n��K?�doN b�[��.��[���M�(����X={�()��3�<���R���l�x��|Cd}p�����>���]�\�������Nw��x�i��������x�Bd��H0��s��b�.���'H�,������tYp�k�/������u��ZL���L�2�'C7��	9��k%>D�l	�Y��J)B��W��r��TCzm��V2A�0f	�Dc�'�����>������U���,����e�9�.A}�r�o?��O�; -�U>Y�8� @���ys!$�p�����X�h��cCP��m�s���R�+�FJ�&�'�z0����B��9����s�svx�Z���������x��V�Y
��8�=����1�s����'*�=�l�m�Tj���^�f����% ��^�L%�#�ctb�������J�O�}�a�SnRgz�a!������x+��&8�e�>��bm��%��R�������k��}@���R@SfX�s���a�����$��9��������9�eI�:�b�bU�������Z�%o0�0�1����/��=yaQ0H���$#�_����v��D�p�G����"I|�0�z�|�wX�n�y�������G?�2C7���z�%���dN�T>������7��V��"]���-{,����.���"M�������(d�#	E	������v=������Tv:�~������)�V��"�< e�YE��5Kr�����![�'G1�iNE������[�8���P�{a"'[��rNzR$%�hQ0m�e1�m���D%[f��8�J��J�j�~;8�(I6&W��t�a��v���0�b:�6�=G���^2H�U���b(��m����_g�����x��V����~
�6��w�!�i����=������B�/���a�5�G��0[	6�d%/����/�	�3�����u��\�J�����LW�m�q������u��;�^����1�C~�:�������;�M�_�?��HGLH����|��	0�I�^=���:�qv���_���R�H�O*L�<5B!kd|�>�������l�`M�~j+.j��*C;�_�����^�g���S�P�q�f�]�yf&��u�A�����B�j�u�D����b�YLt�0N�(!�5m-��IV=G�M���
�
 �/��"����:��R�\��>��5�\��y���p�&RQqIv����=�s���U	�J7���o�]\a�a�o7@�(��^+�_�W��~���_���U��2Q���8X����8�:������+��O��Q��W<^� 9����s�f���m���u��#�3J-�B�at|�����l���$��������r)i��+�r�P��-��D�e
7ai�)+'��4���
m%bB���^�]�`[V��K,Du��V�8F����������^�~#>n*X�W����L#��DT�\��_�v	?Z���MQ1�l~�Eoh�S������vl/��j
������n�e�84�����=��v��,2�/�z���A}����YS��)MA�� ��"i��zi�o��pv�e��oO�i�}'�*�[���'b�G�@���3�X��MR�@Jf�����������8�_cnS�Zx���O�we;�{(6v&��'�7�6a;���	���Q������$�\V�+�tJ_T���Ma�Q���7�<���X�yH��k�#eK��l�y{�E�SK,��M��b��D��+H���/��e����SN�9"�u��$��`�s��N�����X�������$#:'��&���.lOT�J�fv���Yz2%N�V,~���SE��z�8��ep���z��k���=<�-��2����J �K.���b�EAaz+�Q��Z�����8�2�������Jd(;�2���[KP�������c.�joI��>N�KM����v����{����t
��+����qB�#�`m`�S��
ic�C�����5��Nv:>�{�Z���]����d�N2��f���
Bg*g��������L�vx*�����!��L��T��Z~�'J��]T��� J�O��2A��Y��^����z2�EL���.f�B�d���u���~�7n�jW��A8B�f����V=t1lV�=��S!M��m�6�3m�&]Z����������]a����I����V����HK��0�/3����S�>�pv	�l��8�j��_�l��fc�QE�x������w�W�isx���6��|���0��/O�����$W{y��,��@`Sp]�+�:x9[�?���"�'��v~��E��V+_����
�b��+$�*�g7���X�8��B4���
#����8�rhW�	J���x�����������ZvQ,9�t)W*6�?��u�\D��L\~�C���*?��@m$U����k/S:]�V��j7����z�N������g�rn O�h��"�p��������Ts&����D����<_d�K^A���%���nuq�����c���*�~�<��_����Z���(�����\E�R~���_E#H�!������S����s���L�N�U��z����~U��>����%�,���Y����g������H\���00�A!�_��(f_8����yV}�"c�-`����.����"J�0��Q)�c�#�����[n��UG�O����7��t�~sJ4� H�+�Ezt���)�������S=����"��2@��E|�|\�ZAF���������������������lN2��%������/:�]hR��oN��;��X��L��!`��bb �E�N��N�2BY���K�03+O�����(6�NM���R)�f�N�o�g}*�f�V�ZmM��F���,��
��f��'��k��H+����0:��h������Uy�Z-V�9Z�_���kT�
Ed3���|�Z��b�U&_����q���Z�N�9��3v�$;a�S��:���;rk�����/Gh4���&]�{�d`x9���*Y������������Q�<���=J�����R��B���&��K��fd�lF���x��q����O���b�@a���.��k�k5�e��\�������-_O��M����]�0��?�QNlM�g��V/��x/g�@�g\�D�TQv���U�bBLh^��C�c���.������U�[������e�'w3��}��QZ�X���/Q@	�1MN���9%��Jiul����4����0c`[�c<�
2K;��
�f_��^xo���Y��2B���hpY��;c7�����4���d�1i���U�����]���Y��4ZkU"A}�r<��i������C_�����N�P5�]^��^|������3
-rW+�''�i��0��h�SQ?���K���N\d���wj}�	��Z��_��N)�~���f�(H�U��4�KD��C-TrL�A�B&p��"�9'e�'����C��_s�(`d< �%_�F�z'e� ��3�b��6��vkA��-����
���8f�lf{��C�Q��gD�Wk9=�_6��F������W�*��P��n���������R[���g[n�����'�_d@�������O��������
�R`�c�_5mTO:�6z��j�c��W�I�//r�q1��&Z���s<��!3J�q��C��
D�@��h��� ����=�r_i/^cGP9����~U/Z��S��w`�b��|x�)J���`�o��\g
+�����9h����R��h,�Hu��^mf���2vx�z�A����R1��A4���������ckX����K(*\�/��| ����Kh*.����@���������gj[z�A0A.�>q�9�C����hT����m��m������������<�����t%I����Z��pG��c,W�T��O�z�V�;~##RG�}�����w�O�����������U���,��������'����v2�������XL�M��-����e��_�&�{��Np���;�8{v}�Xl�7x�1na���@0���D��E�V��8e�^��h�r\�@����/�����G���-79��89I��\l$#~r�����P�>c�P��
�i@D��E�p=�`��V{�J34��"�y;��k��D�!�0�F��X%�
��QB����3q�m���[����/b�~����O����p.j���$��e�E�X

�b(w��b3�xWbu M
�
:X�a@�P,�-���7��gU���\a�XH�X�>�~�����Vp�����G���x�����:�,7���vF�e=��hg������+��K%��;<B�`����~�?�����V�	��h^rJ��L�H�����wq�x�?n'��*?(z��W9$v+��U�	jU���{j��V����z0����<m��8�%������/
��RG���^��A��[�7HVQ���1�Ar��^�9����d�F@'��C�l���j$����{�,B/��bQ��*�&�is~Y�>�@��j�K�*�A�s?dE�J+�=�����_W<IAq-�<����������9��U��=��-�����|����'�
7,�	@@,�	�S���vk�����C���V�,0��Z��������u?Bh�(�A���,�$����o�������q���!��D�����7!��v�,3z���N~��sE��~1t��P�ch&0<�&c|���=�A����o���]�*n����������{ W�����{m���%�������(+M�
a3/\���nP��\.���FY�!�A�5�]|�cg��jM�f�s����3�l����l5l��:������6^��Z{���8�m�5�S����R����UoL!(����I<��OW�P�`y{�[[�(�3F.M����O�@k�l.7W�q�=�d����ZQ�(xd�z�7{Iy����3��_,`���W����`��^���-�4�}�����*a�XY��O�y��Y���k.�����.�g��W������^tT������B���ad�@��	�I�m����dbXj+D1=?�1c�yVv>���R
����5��*'���`�LW�7��
�^Yd[��*�!{��o������<]���F��_|E)�
�(g$C����`���4<�N�&	&��A�^�|bX���"r��$����<�i��+���E��>I�b�;��fj�TW���_m�G��_o"xT��M��<�{V����k?��EL�������#���,����{v�������'�������6�h������������+4���{���8�g���>�D�����(*�G�w�������n�S-���'������'wKJ�'���	ra�P1�,��kc�����CeLL�"�y���@/�=*��|W�J������r�o%"!H��Z�9��68����iw+�s���a,%��X�q�/��,�z�[��(E���js���r��r�w�1�����E���5rw�����������1�v1���$�z9���Il�8�s�{��
�*o
;\��O\U�\�QC�&QAJ����R��6�t&������k�G;*���Q3���j��+g��be�k�����bCK���)��4uM����:E�8�� ^R?n(j�����_gL��;;�^F�\@���f��M�.;����5�/`q?B5��'^Y��t��f�����V��?�`wyv@�`���=Nu18^z*�E��y>��H���o�IL�����RH�����Rp�q��a��Rr�UT�]*�cSJ��)V�v��6�^�v�@`����W��*�X
���M�@�!��J4���F�c��F���{O<0mL���D�Ho�W�8~�{�BH�~2n����G������`5i�Gp�&q�9p)VE/��7��0�;i5~�����m�� B����9�_�)����8�>i/j�IZ�=��*�XNV	���:�/R��,��D#_���_f�U��\HV+d&8��_�{�bf��A����(�?����mf~���+mm�6z+mYV�����MA��T��?�$���EIc�<�Om:�`��V
�BJ�?��w�j}�j��:��J����4����7eu$�\�QRA����?�l|K�s1!o1j����`�����TJ���h��K��3�z~2=8_4j��?����Wu����^����6A
?������|�������OX-Av���X/v��/6��`�2�d\��x:��"]d���G��"0��������I
#107Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#106)
Re: Command Triggers

On Thu, Feb 16, 2012 at 12:42 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Fixed, I've spent quite some time refining the API.

That is better.

What could still be done here is to create a cache (along the lines of
attoptcache.c) that stores a big array with one slot for each
supported command type. The first time a command is executed in a
particular session, we construct a cache entry for that command type,
storing the OIDs of the before and after triggers. Then, instead of
having to do a catalog scan to notice that there are no triggers for a
given command type, we can just reference into the array and see, oh,
we probed before and found no triggers. CacheRegisterSyscacheCallback
or some other mechanism can be used to flush all the cached
information whenever we notice that pg_cmdtrigger has been updated.

Now, I don't know for sure that this is necessary without performance
testing, but I think we ought to try to figure that out. This version
doesn't apply cleanly for me; there is a conflict in functioncmds.c,
which precludes my easily benchmarking it.

It strikes me that this patch introduces a possible security hole: it
allows command triggers to be installed by the database owner, but
that seems like it might allow the database owner can usurp the
privileges of any user who runs one of these commands in his or her
database, including the superuser. Perhaps that could be fixed by
running command triggers as the person who created them rather than as
the superuser, but that seems like it might be lead to other kinds of
privilege-escalation bugs.

If I install a command trigger that prevents all DDL, how do I
uninstall it? Or say I'm the superuser and I want to undo something
my disgruntled DBA did before he quit.

I would much prefer to have DropCmdTrigStmt wedge itself into the
existing DropStmt infrastructure which I just recently worked so hard
on. If you do that, you should find that you can then easily also
support comments on command triggers, security labels on command
triggers (though I don't know if that's useful), and the ability to
include command triggers in extensions.

I am a bit worried about supporting command triggers on statements
that do internal transaction control, such as VACUUM and CREATE INDEX
CONCURRENTLY. Obviously that's very useful and I'd like to have it,
but there's a problem: if the AFTER trigger errors out, it won't undo
the whole command. That might be very surprising. BEFORE triggers
seem OK, and AFTER triggers might be OK too but we at least need to
think hard about how to document that.

I think it would be better to bail on trying to use CREATE TRIGGER and
DROP TRIGGER as a basis for this functionality, and instead create
completely new toplevel statements CREATE COMMAND TRIGGER and DROP
COMMAND TRIGGER. Then, you could decide that all command triggers
live in the same namespace, and therefore to get rid of the command
trigger called bob you can just say "DROP COMMAND TRIGGER bob",
without having to specify the type of command it applies to. It's
still clear that you're dropping a *command* trigger because that's
right in the statement name, whereas it would make me a bit uneasy to
decide that "DROP TRIGGER bob" means a command trigger just by virtue
of the fact that no table name was specified. That would probably
also make it easier to accomplish the above-described goal of
integrating this into the DropStmt infrastructure.

+       if (!superuser())
+               if (!pg_database_ownercheck(MyDatabaseId, GetUserId()))
+                       aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_DATABASE,
+
get_database_name(MyDatabaseId));

The separate superuser check is superfluous; pg_database_ownercheck()
already handles that.

Can we call InitCommandContext in some centralized place, rather than
separately at lots of different call sites?

I am confused why this is adding a new file called dumpcatalog.c which
looks suspiciously similar to some existing pg_dump code I've been
staring at all day.

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

#108Alvaro Herrera
alvherre@commandprompt.com
In reply to: Dimitri Fontaine (#106)
Re: Command Triggers

Excerpts from Dimitri Fontaine's message of jue feb 16 14:42:26 -0300 2012:

Hi,

Please find attached version 8 of the patch, which fixes most of your
complaints.

Hi,

I didn't like the new cmdtrigger.h file. It's included by a lot of
other headers, and it's also itself including execnodes.h and
parsenodes.h which means practically the whole lot of the source tree
is included in turn. If you could split it, so that the struct
definition is in a different file that's only included by the few .c
files that actually use that struct, I'd be much happier.

... after looking at it more closely, I think only this line needs to be
in a separate file:

typedef struct CommandContextData *CommandContext;

and that file is included by other headers; they don't need the function
declarations or the struct definition.

--
Álvaro Herrera <alvherre@commandprompt.com>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support

#109Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#107)
Re: Command Triggers

Hi,

First answer now, new patch version tomorrow.

Robert Haas <robertmhaas@gmail.com> writes:

That is better.

Cool :)

What could still be done here is to create a cache (along the lines of
attoptcache.c) that stores a big array with one slot for each
supported command type. The first time a command is executed in a
particular session, we construct a cache entry for that command type,
storing the OIDs of the before and after triggers. Then, instead of
having to do a catalog scan to notice that there are no triggers for a
given command type, we can just reference into the array and see, oh,
we probed before and found no triggers. CacheRegisterSyscacheCallback
or some other mechanism can be used to flush all the cached
information whenever we notice that pg_cmdtrigger has been updated.

I guess it's another spelling for a catalog cache, so I'll look at what
it takes to build either a full catcache or this array cache you're
talking about tomorrow.

Now, I don't know for sure that this is necessary without performance
testing, but I think we ought to try to figure that out. This version
doesn't apply cleanly for me; there is a conflict in functioncmds.c,
which precludes my easily benchmarking it.

Means I need to update my master's branch and merge conflicts. You could
also test right from my github branch too, I guess.

https://github.com/dimitri/postgres
https://github.com/dimitri/postgres/tree/command_triggers

It strikes me that this patch introduces a possible security hole: it
allows command triggers to be installed by the database owner, but
that seems like it might allow the database owner can usurp the
privileges of any user who runs one of these commands in his or her
database, including the superuser. Perhaps that could be fixed by
running command triggers as the person who created them rather than as
the superuser, but that seems like it might be lead to other kinds of
privilege-escalation bugs.

We could decide command triggers are superuser only. Security is not
something I'm very strong at, so I'll leave it up to you to decide.

If I install a command trigger that prevents all DDL, how do I
uninstall it? Or say I'm the superuser and I want to undo something
my disgruntled DBA did before he quit.

Good catch, I guess we need to remove creating and dropping a command
trigger to the list of supported commands in the ANY COMMAND list.

I would much prefer to have DropCmdTrigStmt wedge itself into the
existing DropStmt infrastructure which I just recently worked so hard
on. If you do that, you should find that you can then easily also
support comments on command triggers, security labels on command
triggers (though I don't know if that's useful), and the ability to
include command triggers in extensions.

Ah yes, that too was on the TODO list, I just forgot about it. I still
remember the merge conflicts when that patch went it, you know… :)

I am a bit worried about supporting command triggers on statements
that do internal transaction control, such as VACUUM and CREATE INDEX
CONCURRENTLY. Obviously that's very useful and I'd like to have it,
but there's a problem: if the AFTER trigger errors out, it won't undo
the whole command. That might be very surprising. BEFORE triggers
seem OK, and AFTER triggers might be OK too but we at least need to
think hard about how to document that.

I think we should limit the support to BEFORE command trigger only. It
was unclear to me how to solve the problem for the AFTER command case,
and if you're unclear to, then there's not that many open questions.

I think it would be better to bail on trying to use CREATE TRIGGER and
DROP TRIGGER as a basis for this functionality, and instead create
completely new toplevel statements CREATE COMMAND TRIGGER and DROP
COMMAND TRIGGER. Then, you could decide that all command triggers
live in the same namespace, and therefore to get rid of the command
trigger called bob you can just say "DROP COMMAND TRIGGER bob",
without having to specify the type of command it applies to. It's

I have no strong feeling about that. Would that require that COMMAND be
more reserved than it currently is (UNRESERVED_KEYWORD)?

still clear that you're dropping a *command* trigger because that's
right in the statement name, whereas it would make me a bit uneasy to
decide that "DROP TRIGGER bob" means a command trigger just by virtue
of the fact that no table name was specified. That would probably
also make it easier to accomplish the above-described goal of
integrating this into the DropStmt infrastructure.

The other thing is that you might want to drop the trigger from only one
command, of course we could support both syntax. We could also add
support for the following one:

DROP TRIGGER bob ON ALL COMMANDS;

As ALL is already a reserved word, that will work.

+       if (!superuser())
+               if (!pg_database_ownercheck(MyDatabaseId, GetUserId()))
+                       aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_DATABASE,
+
get_database_name(MyDatabaseId));

The separate superuser check is superfluous; pg_database_ownercheck()
already handles that.

Ok, let's see if we keep the feature open to database owners then.

Can we call InitCommandContext in some centralized place, rather than
separately at lots of different call sites?

Then you have to either make the current command context a backend
private global variable, or amend even more call sites to pass it down.
The global variable idea does not work, see RemoveRelations() and
RemoveObjects() which are handling an array of command contexts.

So do you prefer lots of InitCommandContext() or adding another parameter
to almost all functions called by standard_ProcessUtility()?

I am confused why this is adding a new file called dumpcatalog.c which
looks suspiciously similar to some existing pg_dump code I've been
staring at all day.

Merge or diff issue I think, I will look into that, I don't know.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#110Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Alvaro Herrera (#108)
Re: Command Triggers

Alvaro Herrera <alvherre@commandprompt.com> writes:

I didn't like the new cmdtrigger.h file. It's included by a lot of
other headers, and it's also itself including execnodes.h and
parsenodes.h which means practically the whole lot of the source tree
is included in turn. If you could split it, so that the struct
definition is in a different file that's only included by the few .c
files that actually use that struct, I'd be much happier.

I didn't realize that, thanks for reviewing!

... after looking at it more closely, I think only this line needs to be
in a separate file:

typedef struct CommandContextData *CommandContext;

and that file is included by other headers; they don't need the function
declarations or the struct definition.

I'll look into that tomorrow then. The same trick is already applied to
Relation and RelationData (resp. in src/include/utils/relcache.h and
src/include/utils/rel.h), and only now I understand why :)

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#111Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#109)
Re: Command Triggers

On Thu, Feb 16, 2012 at 4:21 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

We could decide command triggers are superuser only.  Security is not
something I'm very strong at, so I'll leave it up to you to decide.

That's certainly the easiest option. If you don't feel passionate
about spending a lot of energy figuring out how to make it secure,
then I suggest we just restrict it to superusers until someone does.

If I install a command trigger that prevents all DDL, how do I
uninstall it?  Or say I'm the superuser and I want to undo something
my disgruntled DBA did before he quit.

Good catch, I guess we need to remove creating and dropping a command
trigger to the list of supported commands in the ANY COMMAND list.

Another option would be to add a PGC_SUSET boolean GUC that can be
used to disable command triggers. I think that might be more
flexible, not to mention useful for recursion prevention.

I am a bit worried about supporting command triggers on statements
that do internal transaction control, such as VACUUM and CREATE INDEX
CONCURRENTLY.  Obviously that's very useful and I'd like to have it,
but there's a problem: if the AFTER trigger errors out, it won't undo
the whole command.  That might be very surprising.  BEFORE triggers
seem OK, and AFTER triggers might be OK too but we at least need to
think hard about how to document that.

I think we should limit the support to BEFORE command trigger only.  It
was unclear to me how to solve the problem for the AFTER command case,
and if you're unclear to, then there's not that many open questions.

Works for me.

I think it would be better to bail on trying to use CREATE TRIGGER and
DROP TRIGGER as a basis for this functionality, and instead create
completely new toplevel statements CREATE COMMAND TRIGGER and DROP
COMMAND TRIGGER.  Then, you could decide that all command triggers
live in the same namespace, and therefore to get rid of the command
trigger called bob you can just say "DROP COMMAND TRIGGER bob",
without having to specify the type of command it applies to.  It's

I have no strong feeling about that.  Would that require that COMMAND be
more reserved than it currently is (UNRESERVED_KEYWORD)?

It shouldn't.

still clear that you're dropping a *command* trigger because that's
right in the statement name, whereas it would make me a bit uneasy to
decide that "DROP TRIGGER bob" means a command trigger just by virtue
of the fact that no table name was specified.  That would probably
also make it easier to accomplish the above-described goal of
integrating this into the DropStmt infrastructure.

The other thing is that you might want to drop the trigger from only one
command, of course we could support both syntax.  We could also add
support for the following one:

 DROP TRIGGER bob ON ALL COMMANDS;

Uh, hold on. Creating a trigger on multiple commands ought to only
create one entry in pg_cmdtrigger. If you drop it, you drop the whole
thing. Changing which commands the trigger applies to would be the
job for ALTER, not DROP. But note that we have no similar
functionality for regular triggers, so I can't think we really need it
here either.

Can we call InitCommandContext in some centralized place, rather than
separately at lots of different call sites?

Then you have to either make the current command context a backend
private global variable, or amend even more call sites to pass it down.
The global variable idea does not work, see RemoveRelations() and
RemoveObjects() which are handling an array of command contexts.

So do you prefer lots of InitCommandContext() or adding another parameter
to almost all functions called by standard_ProcessUtility()?

Blech. Maybe we should just have CommandFiresTriggers initialize it
if not already done?

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

#112Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#111)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

On Thu, Feb 16, 2012 at 4:21 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:
That's certainly the easiest option. If you don't feel passionate
about spending a lot of energy figuring out how to make it secure,
then I suggest we just restrict it to superusers until someone does.

Works for me.

If I install a command trigger that prevents all DDL, how do I
uninstall it?  Or say I'm the superuser and I want to undo something
my disgruntled DBA did before he quit.

Good catch, I guess we need to remove creating and dropping a command
trigger to the list of supported commands in the ANY COMMAND list.

Another option would be to add a PGC_SUSET boolean GUC that can be
used to disable command triggers. I think that might be more
flexible, not to mention useful for recursion prevention.

Wait, we already have ALTER TRIGGER bob ON ANY COMMAND SET DISABLED;
which I had forgotten about in the previous answer, so I think we're
good as it is. That's how I prevented recursion in some of my tests
(not included in the regress tests, was using INSTEAD OF).

 DROP TRIGGER bob ON ALL COMMANDS;

Uh, hold on. Creating a trigger on multiple commands ought to only
create one entry in pg_cmdtrigger. If you drop it, you drop the whole
thing. Changing which commands the trigger applies to would be the
job for ALTER, not DROP. But note that we have no similar
functionality for regular triggers, so I can't think we really need it
here either.

Why would we do it that way (a single entry for multiple commands)? The
way it is now, it's only syntactic sugar, so I think it's easier to
implement, document and use.

So do you prefer lots of InitCommandContext() or adding another parameter
to almost all functions called by standard_ProcessUtility()?

Blech. Maybe we should just have CommandFiresTriggers initialize it
if not already done?

Thing is, in a vast number of cases (almost of ALTER OBJECT name,
namespace and owner) you don't have the Node * parse tree any more from
the place where you check for CommandFiresTriggers(), so you can't
initialize there. That's part of the fun.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#113Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#112)
Re: Command Triggers

On Thu, Feb 16, 2012 at 5:38 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Another option would be to add a PGC_SUSET boolean GUC that can be
used to disable command triggers.  I think that might be more
flexible, not to mention useful for recursion prevention.

Wait, we already have ALTER TRIGGER bob ON ANY COMMAND SET DISABLED;
which I had forgotten about in the previous answer, so I think we're
good as it is.  That's how I prevented recursion in some of my tests
(not included in the regress tests, was using INSTEAD OF).

Eh, so what happens then if someone sets a command trigger on ALTER TRIGGER?

 DROP TRIGGER bob ON ALL COMMANDS;

Uh, hold on.  Creating a trigger on multiple commands ought to only
create one entry in pg_cmdtrigger.  If you drop it, you drop the whole
thing.  Changing which commands the trigger applies to would be the
job for ALTER, not DROP.  But note that we have no similar
functionality for regular triggers, so I can't think we really need it
here either.

Why would we do it that way (a single entry for multiple commands)?  The
way it is now, it's only syntactic sugar, so I think it's easier to
implement, document and use.

Well, for one thing, it's consistent with how we handle it for regular
triggers. For two things, if you create an object named bob, you
expect to end up with an object named bob - not 47 objects (or
whatever) that are all named bob. Also, suppose you create a trigger
on ALL COMMANDS, and then a new version of PG adds a new command.
When you dump and reload, do you expect to end up with a trigger on
all commands that existed in the old version, or all the commands that
exist in the new version? Or conversely, suppose we get rid of a
command in a future release. How will we handle that? I can't think
of another example of where a CREATE command creates multiple objects
like that.

So do you prefer lots of InitCommandContext() or adding another parameter
to almost all functions called by standard_ProcessUtility()?

Blech.  Maybe we should just have CommandFiresTriggers initialize it
if not already done?

Thing is, in a vast number of cases (almost of ALTER OBJECT name,
namespace and owner) you don't have the Node * parse tree any more from
the place where you check for CommandFiresTriggers(), so you can't
initialize there. That's part of the fun.

Hmm, I'll look at this in more detail next time through.

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

#114Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#113)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

Wait, we already have ALTER TRIGGER bob ON ANY COMMAND SET DISABLED;

Eh, so what happens then if someone sets a command trigger on ALTER TRIGGER?

We should remove support for command triggers on alter command triggers.
Well I could also go with the GUC idea, it's only that I'm not entirely
sold it's the best we can do yet and I'd like to avoid yet another GUC.

Why would we do it that way (a single entry for multiple commands)?  The
way it is now, it's only syntactic sugar, so I think it's easier to
implement, document and use.

Well, for one thing, it's consistent with how we handle it for regular
triggers. For two things, if you create an object named bob, you

I don't think so, if you attach the same procedure to more than one
table each time with the same name, you get multiple entries in
pg_trigger:

"pg_trigger_tgrelid_tgname_index" UNIQUE, btree (tgrelid, tgname)

create trigger footg after insert on tg.foo for each row execute procedure tg.trigfunc();
create trigger footg after insert on tg.bar for each row execute procedure tg.trigfunc();
create trigger footg after insert on tg.baz for each row execute procedure tg.trigfunc();

select oid, tgrelid::regclass, tgname, tgfoid, tgtype, tgenabled from pg_trigger;
oid | tgrelid | tgname | tgfoid | tgtype | tgenabled
--------+---------+--------+--------+--------+-----------
533210 | tg.foo | footg | 533209 | 5 | O
533211 | tg.bar | footg | 533209 | 5 | O
533212 | tg.baz | footg | 533209 | 5 | O
(3 rows)

The difference I see is that in the table trigger case you don't have a
syntax that allows you to do the 3 operations I did above in 1 command,
and it's easy to provide for this capability with command triggers (and
the use case is much bigger too, as all command triggers are given the
same arguments and all expected to return void).

expect to end up with an object named bob - not 47 objects (or
whatever) that are all named bob. Also, suppose you create a trigger
on ALL COMMANDS, and then a new version of PG adds a new command.

You create a trigger on ANY command :)

When you dump and reload, do you expect to end up with a trigger on
all commands that existed in the old version, or all the commands that
exist in the new version? Or conversely, suppose we get rid of a
command in a future release. How will we handle that? I can't think
of another example of where a CREATE command creates multiple objects
like that.

ANY COMMAND triggers are just one entry in pg_cmdtrigger, with the
command name registered as "ANY", which is only safe as long as we don't
provide a new SQL command whose command tag is ANY. We could decide that
we want to name this magic ANY command "__ANY__", but it does not look
like it fits the project usual naming style.

--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#115Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Alvaro Herrera (#108)
Re: Command Triggers

Alvaro Herrera <alvherre@commandprompt.com> writes:

... after looking at it more closely, I think only this line needs to be
in a separate file:

typedef struct CommandContextData *CommandContext;

Files like src/backend/commands/tablecmds.c and others need both the
structure and the pointer, so we need both. What about putting those
definitions into src/include/catalog/pg_cmdtrigger.h?

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#116Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Alvaro Herrera (#108)
Re: Command Triggers

Alvaro Herrera <alvherre@commandprompt.com> writes:

I didn't like the new cmdtrigger.h file. It's included by a lot of
other headers, and it's also itself including execnodes.h and

It turns around that this file does not need including execnode.h, I've
cleaned that up now (compile ok, make installcheck ok).

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#117Marko Kreen
markokr@gmail.com
In reply to: Dimitri Fontaine (#114)
Re: Command Triggers

On Fri, Feb 17, 2012 at 10:54 AM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

Wait, we already have ALTER TRIGGER bob ON ANY COMMAND SET DISABLED;

Eh, so what happens then if someone sets a command trigger on ALTER TRIGGER?

We should remove support for command triggers on alter command triggers.
Well I could also go with the GUC idea, it's only that I'm not entirely
sold it's the best we can do yet and I'd like to avoid yet another GUC.

Btw, we already have a GUC for triggers: session_replication_role,
how will the command triggers follow that?

--
marko

#118Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Marko Kreen (#117)
Re: Command Triggers

Marko Kreen <markokr@gmail.com> writes:

Btw, we already have a GUC for triggers: session_replication_role,
how will the command triggers follow that?

Note that the replica here in my mind would have been an Hot Standby
node, and having the standby run the replica/always command triggers is
not implemented yet, because you can't run DDL on the standby.

Now that you mention it we should also provide support the GUC here and
only fire the triggers matching it. I'm working on that now.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#119Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#114)
Re: Command Triggers

On Fri, Feb 17, 2012 at 3:54 AM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

Wait, we already have ALTER TRIGGER bob ON ANY COMMAND SET DISABLED;

Eh, so what happens then if someone sets a command trigger on ALTER TRIGGER?

We should remove support for command triggers on alter command triggers.
Well I could also go with the GUC idea, it's only that I'm not entirely
sold it's the best we can do yet and I'd like to avoid yet another GUC.

I'm OK with not supporting command triggers on command triggers, but I
still think the GUC is useful. Keep in mind that flipping a GUC is
really cheap compared to a catalog change, and can affect just one
session. Those are significant advantages. However, if you want to
just not support triggers on statements that modify command triggers,
I'm OK with that, too.

Why would we do it that way (a single entry for multiple commands)?  The
way it is now, it's only syntactic sugar, so I think it's easier to
implement, document and use.

Well, for one thing, it's consistent with how we handle it for regular
triggers. For two things, if you create an object named bob, you

I don't think so, if you attach the same procedure to more than one
table each time with the same name, you get multiple entries in
pg_trigger:

   "pg_trigger_tgrelid_tgname_index" UNIQUE, btree (tgrelid, tgname)

create trigger footg after insert on tg.foo for each row execute procedure tg.trigfunc();
create trigger footg after insert on tg.bar for each row execute procedure tg.trigfunc();
create trigger footg after insert on tg.baz for each row execute procedure tg.trigfunc();

Sure, but if you run the same trigger on multiple operations - INSERT
OR UPDATE OR DELETE.

expect to end up with an object named bob - not 47 objects (or
whatever) that are all named bob.  Also, suppose you create a trigger
on ALL COMMANDS, and then a new version of PG adds a new command.

You create a trigger on ANY command :)

Oh. Well, then +1 for me on the ANY COMMAND thing, but -1 on ALL
COMMANDS. I can't see that there's enough utility to having a
bulk-create functionality to justify its existence. The ANY COMMAND
thing I think is what people will want.

ANY COMMAND triggers are just one entry in pg_cmdtrigger, with the
command name registered as "ANY", which is only safe as long as we don't
provide a new SQL command whose command tag is ANY. We could decide that
we want to name this magic ANY command "__ANY__", but it does not look
like it fits the project usual naming style.

I am thinking that we should ditch the idea of keeping track of
commands using strings and instead assign a bunch of integer constants
using a big enum. The parser can translate from what the user enters
to these constants and then use those throughout, including in the
system catalogs.

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

#120Marko Kreen
markokr@gmail.com
In reply to: Dimitri Fontaine (#118)
Re: Command Triggers

On Fri, Feb 17, 2012 at 4:04 PM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Marko Kreen <markokr@gmail.com> writes:

Btw, we already have a GUC for triggers: session_replication_role,
how will the command triggers follow that?

Note that the replica here in my mind would have been an Hot Standby
node, and having the standby run the replica/always command triggers is
not implemented yet, because you can't run DDL on the standby.

But we will be able? Thats news to me.

I'm more interested whether it follows ordinary trigger
behaviour on Slony/Londiste slave node.

--
marko

#121Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#111)
1 attachment(s)
Re: Command Triggers

Hi,

Please find attached version 9 of this patch, answering to most open
comments about it.

Robert Haas <robertmhaas@gmail.com> writes:

That's certainly the easiest option. If you don't feel passionate
about spending a lot of energy figuring out how to make it secure,
then I suggest we just restrict it to superusers until someone does.

Done.

Good catch, I guess we need to remove creating and dropping a command
trigger to the list of supported commands in the ANY COMMAND list.

Done.

Another option would be to add a PGC_SUSET boolean GUC that can be
used to disable command triggers. I think that might be more
flexible, not to mention useful for recursion prevention.

I implemented support for the session_replication_role GUC so that you
can SET session_replication_role TO replica; and avoid any command
trigger running (if you didn't ALTER them from their default setting).

Of course you can also use the GUC in its intended purpose, as said
Marko.

[CREATE INDEX CONCURRENTLY and VACUUM]

I think we should limit the support to BEFORE command trigger only.  It
was unclear to me how to solve the problem for the AFTER command case,
and if you're unclear to, then there's not that many open questions.

Works for me.

Done. Of course at the time the command trigger is created you can't
distinguish if the CREATE INDEX command will be run CONCURRENTLY or not,
so I've decided to issue a WARNING about it.

I think it would be better to bail on trying to use CREATE TRIGGER and
DROP TRIGGER as a basis for this functionality, and instead create
completely new toplevel statements CREATE COMMAND TRIGGER and DROP
COMMAND TRIGGER.  Then, you could decide that all command triggers
live in the same namespace, and therefore to get rid of the command
trigger called bob you can just say "DROP COMMAND TRIGGER bob",
without having to specify the type of command it applies to.  It's

Not done yet, I'm not sure we took a decision on this one. Also note
that I chose to give DDL on command triggers their own command tag so
that it's easy to separate them out in the command trigger context.

Robert Haas <robertmhaas@gmail.com> writes:

Wait, we already have ALTER TRIGGER bob ON ANY COMMAND SET DISABLED;

Eh, so what happens then if someone sets a command trigger on ALTER TRIGGER?

You would need to set a command trigger on ALTER COMMAND TRIGGER and
that's not supported. Triggers on command "ALTER TRIGGER" in fact will
not get fired on ALTER TRIGGER ... ON COMMAND ...

I guess that's a point to change the grammar the way you're hinting:

CREATE COMMAND TRIGGER
DROP COMMAND TRIGGER
ALTER COMMAND TRIGGER

That also needs each their own reference page. It will be easier on the
users I guess. Will work on that.

Robert Haas <robertmhaas@gmail.com> writes:

I'm OK with not supporting command triggers on command triggers, but I
still think the GUC is useful. Keep in mind that flipping a GUC is
really cheap compared to a catalog change, and can affect just one
session. Those are significant advantages. However, if you want to
just not support triggers on statements that modify command triggers,
I'm OK with that, too.

Both done, if you agree with using session_replication_role here.

Sure, but if you run the same trigger on multiple operations - INSERT
OR UPDATE OR DELETE.

I failed to see that analogy. The other problem with the current way of
doing things is that I can't integrate with RemoveObjects(), and I think
you won't like that :)

You create a trigger on ANY command :)

Oh. Well, then +1 for me on the ANY COMMAND thing, but -1 on ALL
COMMANDS. I can't see that there's enough utility to having a
bulk-create functionality to justify its existence. The ANY COMMAND
thing I think is what people will want.

There's no such thing as ALL COMMANDS in the patch, there's a syntactic
sugar allowing you to create and drop more than one command trigger in a
single command, much as we have DROP TABLE foo, bar, baz;

I am thinking that we should ditch the idea of keeping track of
commands using strings and instead assign a bunch of integer constants
using a big enum. The parser can translate from what the user enters
to these constants and then use those throughout, including in the
system catalogs.

It's not really command strings but the Command Tag we've historically
been using up until now. You're saying that it should remain the same
for users but change internally. No strong opinion from me here, apart
from it being more code for doing the same thing.

I mean we will need to get the command tag from the parse tree then
change that into an integer thanks to yet another huge switch that will
have to be maintained every time we add a new command. We only have too
many of them now (not proposing to remove some).

Alvaro Herrera <alvherre@commandprompt.com> writes:

I didn't like the new cmdtrigger.h file. It's included by a lot of
other headers, and it's also itself including execnodes.h and

Fixed in the attach.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

Attachments:

command-trigger.v9.patch.gzapplication/octet-streamDownload
#122Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Dimitri Fontaine (#121)
Re: Command Triggers

Dimitri Fontaine <dimitri@2ndQuadrant.fr> writes:

I think it would be better to bail on trying to use CREATE TRIGGER and
DROP TRIGGER as a basis for this functionality, and instead create
completely new toplevel statements CREATE COMMAND TRIGGER and DROP
COMMAND TRIGGER.  Then, you could decide that all command triggers
live in the same namespace, and therefore to get rid of the command
trigger called bob you can just say "DROP COMMAND TRIGGER bob",
without having to specify the type of command it applies to.  It's

I guess that's a point to change the grammar the way you're hinting:

CREATE COMMAND TRIGGER
DROP COMMAND TRIGGER
ALTER COMMAND TRIGGER

That also needs each their own reference page. It will be easier on the
users I guess. Will work on that.

FWIW I've pushed such a change to my github repository, I'm not spamming
the list with v10 already though, unless someone wants to see it.

https://github.com/dimitri/postgres/commit/82996b45aae10f12818f1e3097ba805fff22a97b

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#123Robert Haas
robertmhaas@gmail.com
In reply to: Dimitri Fontaine (#121)
Re: Command Triggers

On Fri, Feb 17, 2012 at 10:42 AM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Done.  Of course at the time the command trigger is created you can't
distinguish if the CREATE INDEX command will be run CONCURRENTLY or not,
so I've decided to issue a WARNING about it.

That seems icky. Whatever warnings need to be given should be in the
documentation, not at runtime.

Another idea here would be to treat CREATE INDEX CONCURRENTLY as if it
were a separate toplevel command, for command-trigger purposes only.
But I'm not sure that's any better.

You would need to set a command trigger on ALTER COMMAND TRIGGER and
that's not supported. Triggers on command "ALTER TRIGGER" in fact will
not get fired on ALTER TRIGGER ... ON COMMAND ...

I guess that's a point to change the grammar the way you're hinting:

Indeed it is. :-)

 CREATE COMMAND TRIGGER
 DROP COMMAND TRIGGER
 ALTER COMMAND TRIGGER

That also needs each their own reference page.  It will be easier on the
users I guess.  Will work on that.

Yeah, I think that will be much more clear, and not really that much
work for you. It will also make the reference pages simpler, I think,
since there are significant behavioral differences between ordinary
triggers and command triggers.

Both done, if you agree with using session_replication_role here.

It's better than a sharp stick in the eye. I'm not convinced it's
ideal, but I don't feel strongly enough about the issue to push on it
for now, as long as we disallow command triggers on CREATE/ALTER/DROP
COMMAND TRIGGER.

Sure, but if you run the same trigger on multiple operations - INSERT
OR UPDATE OR DELETE.

I failed to see that analogy. The other problem with the current way of
doing things is that I can't integrate with RemoveObjects(), and I think
you won't like that :)

I sure won't. I think ultimately you won't like it either, since the
objectaddress infrastructure is also needed to make this work with
extensions. And I assume you would agree with me that extensions are
an important feature. :-)

You create a trigger on ANY command :)

Oh.  Well, then +1 for me on the ANY COMMAND thing, but -1 on ALL
COMMANDS.  I can't see that there's enough utility to having a
bulk-create functionality to justify its existence.  The ANY COMMAND
thing I think is what people will want.

There's no such thing as ALL COMMANDS in the patch, there's a syntactic
sugar allowing you to create and drop more than one command trigger in a
single command, much as we have DROP TABLE foo, bar, baz;

OK, I'll look more carefully.

I am thinking that we should ditch the idea of keeping track of
commands using strings and instead assign a bunch of integer constants
using a big enum.  The parser can translate from what the user enters
to these constants and then use those throughout, including in the
system catalogs.

It's not really command strings but the Command Tag we've historically
been using up until now.  You're saying that it should remain the same
for users but change internally.  No strong opinion from me here, apart
from it being more code for doing the same thing.

Well, the reason I thought it might be better is for caching purposes.
If you have a cache of which triggers need to be run for which
commands, an integer index into an array will be a lot faster than a
hash table lookup. But it may bear more examination, so I don't feel
this is a must-do at this point.

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

#124Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#123)
Re: Command Triggers

Robert Haas <robertmhaas@gmail.com> writes:

On Fri, Feb 17, 2012 at 10:42 AM, Dimitri Fontaine
<dimitri@2ndquadrant.fr> wrote:

Done.  Of course at the time the command trigger is created you can't
distinguish if the CREATE INDEX command will be run CONCURRENTLY or not,
so I've decided to issue a WARNING about it.

That seems icky. Whatever warnings need to be given should be in the
documentation, not at runtime.

Agreed. We're still making user visible changes though, so I wanted to
defer docs editing some more. Even documented, a WARNING seems a good
idea to me, but maybe you would prefer a NOTICE?

Another idea here would be to treat CREATE INDEX CONCURRENTLY as if it
were a separate toplevel command, for command-trigger purposes only.
But I'm not sure that's any better.

The patch as it stands will fire the AFTER command trigger only when not
using the CONCURRENTLY variant, which I think is enough, once documented.

Yeah, I think that will be much more clear, and not really that much
work for you. It will also make the reference pages simpler, I think,
since there are significant behavioral differences between ordinary
triggers and command triggers.

Yeah done this way, still needed an overview section in triggers.sgml I
think.

Both done, if you agree with using session_replication_role here.

It's better than a sharp stick in the eye. I'm not convinced it's
ideal, but I don't feel strongly enough about the issue to push on it
for now, as long as we disallow command triggers on CREATE/ALTER/DROP
COMMAND TRIGGER.

We simply don't support those commands as far as command triggers are
concerned, which seems to be like a sane limitation.

I sure won't. I think ultimately you won't like it either, since the
objectaddress infrastructure is also needed to make this work with
extensions. And I assume you would agree with me that extensions are
an important feature. :-)

How you'd guess about that :)

Will see about it later tonight, I'd like to keep the multiple command
drop command trigger spelling.

It's not really command strings but the Command Tag we've historically
been using up until now.  You're saying that it should remain the same
for users but change internally.  No strong opinion from me here, apart
from it being more code for doing the same thing.

Well, the reason I thought it might be better is for caching purposes.
If you have a cache of which triggers need to be run for which
commands, an integer index into an array will be a lot faster than a
hash table lookup. But it may bear more examination, so I don't feel
this is a must-do at this point.

I've been trying to get a feeling of the runtime performance with
command triggers in the line you suggested, even if I'd be very
surprised that a couple of index scans are anything but noise when
completing a DDL command.

I'm having those results on my development machine:

duration: 30 s
number of transactions actually processed: 42390
tps = 1413.004051 (including connections establishing)
tps = 1413.505517 (excluding connections establishing)
statement latencies in milliseconds:
0.705843 create or replace function plus1(int) returns bigint language sql as $$ select $1::bigint + 1; $$;

I don't have the setup to compare that easily to current master's
branch, I was hoping you would run tests on your side (btw the previous
patch version is rebased against master and cleaned up, should be fine
now — oh and in context format).

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

#125Dimitri Fontaine
dimitri@2ndQuadrant.fr
In reply to: Robert Haas (#123)
1 attachment(s)
Re: Command Triggers

Hi,

Robert Haas <robertmhaas@gmail.com> writes:

That seems icky. Whatever warnings need to be given should be in the
documentation, not at runtime.

So, new patch is attached, with some more docs, but it's still shy on it
though, mainly because I don't have a setup to build docs easily here.
I intend to be completing docs on Monday.

If you agree with the way it's implemented now, I'll integrate some more
commands (all our commands expect for those working with shared objects,
and restricting away after command trigger support for commands that
handle transactions themselves).

Once those two items are done, I believe we will reach the Grail^W
“ready for commit” status, and that should happen early next week.

 CREATE COMMAND TRIGGER
 DROP COMMAND TRIGGER
 ALTER COMMAND TRIGGER

Yeah, I think that will be much more clear, and not really that much
work for you. It will also make the reference pages simpler, I think,
since there are significant behavioral differences between ordinary
triggers and command triggers.

This is implementing in the attached patch, v10.

I failed to see that analogy. The other problem with the current way of
doing things is that I can't integrate with RemoveObjects(), and I think
you won't like that :)

I sure won't. I think ultimately you won't like it either, since the
objectaddress infrastructure is also needed to make this work with
extensions. And I assume you would agree with me that extensions are
an important feature. :-)

So now I've integrated with RemoveObjects(), which means it's no longer
possible to drop a command trigger attached to more than one command in
one go. Not a show stopper, but maybe worth noting here.

It's not really command strings but the Command Tag we've historically
been using up until now.  You're saying that it should remain the same
for users but change internally.  No strong opinion from me here, apart
from it being more code for doing the same thing.

Well, the reason I thought it might be better is for caching purposes.
If you have a cache of which triggers need to be run for which
commands, an integer index into an array will be a lot faster than a
hash table lookup. But it may bear more examination, so I don't feel
this is a must-do at this point.

I'm still not convinced that two index scans will be noticeable when
executing a DDL command. So it all feels like premature optimization to
me, even if I completely understand where you come from (most of the
patch you worked on for this release share an easy to spot common theme
after all).

So I didn't implement a cache here yet in patch v10. I would hate to
spend time on that to chase a 1% DDL-only performance regression.

Regards,
--
Dimitri Fontaine
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

Attachments:

command-trigger.v10.patch.gzapplication/octet-streamDownload
�@O�<�W���?�_����&�<B�s�m�����i��/�G���nd����������CZ�kHH�����������y����E*������ooo�A�	�J������tX����};�L,����;�p����?�L��}D`5�V�������|��,����Q<
i��K��L�[�����}
� ���pF\���6/{�N����l7z���E������	�-��M�����l���wR���w��'����J��B��������&��?J�%]��"��c�U�$��5��{�A��%���t�4	�ADO�1 @I<���C�w��$�~��rx���:ks}�~�]��?����~����	(b�i�N�p�O���I��tG3?�Fn$V?���������eSk�Qb{V���1;�U����� M��Sr�^5I�za^���N�Z!,#����BZ���&��>
�-�(�-�=|
������6�.����/��n�����l�M��hu�4D�����CaS���v\;�I�Fv�NQ�N*�������D�w���n��7"�0��������`�G7�]D�x)�;|��\qn��AB&I#3� '
I&�p8V\��/�����h�wV���z@�d��#�:�
 �	y���z�d1 �
$r���h���qV0 ��N:��8�8�i�\�G�T�[k�� �c���t��V���oG���?��_���-��r���.��@4
�[�iD�nvmfm�a�Q$�� ��z<D[^��{���d
�
\�h�E4p�B�8�3#�y'�S�h�H�N�Z�(V�<Z9�F���9:"w2��!�l
���=����	�hxz���yO}��d;��g�R��
��r�zv�l0)vj3�����+}K��#����+�K��x��N��5��W���r	�z������O?|�/����O���t*��{�����u�I�����l�'b��JD���k�~��t����-_��0�m��5K����'�]2�Q.�o���!����9�z4W:������+3�����lw[��|��m���k�)���n]�u]�v��k��O��v�0��V��|�yq�i^��|��M�c��;�������AH��N�{�mv�7Z�Z��v��^���r%uD��}������y{S����2[r��*yKn�xSq�xka��+����*��<WHX��e�n�_u�
��
�qV���orM���F�UD[�s}�ck(H'kSd�=�B�t�����=�6��Y,�?7;�BkNXKQ���d~�wng�F�e�_�'l�����5/�:.5��I~��77y�����o���a������%�k{����e���q��g##�Cf��>wn�������^����W�=,�'��sJ"��hS,��q������	
l�:��Y�M��]h�p�?��������.iWY�e�L����c+Nq��'L����\�,�4�����x@��	 �b2%�&sh�����!f�f��)>'�q�Y�z�N�l����U�f� ��5��x�&Y@Uf$���,�aS&C+��/�%F
q� $�8%�	2An�KNX���%�$7�k����r�%`����2�D
;8�A&��z�c2�I
%�%�f$��0�Fd4;:l�rg~}?�9?{L'�����q�}>q�h|�`H��+�\�R`|&"�z�J��<i�G1������S/�c�8?�!R�ID��9q�����2$�4���sa�E4���1LJ�a��:`+{����S�����aCv2��B��cW�O�p�f����J�p��<vq��)h�'��#��G+���ZN�#<���%����xN��(�N=<���3st:����S�����K�D��GE2��������J3��� �48XD�5$� 	�L%jL�!*�������[�Q�q
N�B��+1���}�:c��<�"����J��fi�i���(�n%[��o��E�=Y��4W�I��T���x�GE��SJi��/��vg6�v�L��F�dJh�ld��N���t\0�7X_�� %��`f7��"n�R`"�"�LTI�H�0���4�H��[~������8Fd#���w(���%�����G����6�T'a���B��P��)4�Y`	Y��9.����z�aU�B7E`�F#�o��T����@Y-�Ii��;���8Wj\�[��8�3=���IpG?�n��C����Q`RS�����u�p&���D�������N����JZbQ�U��,����>@�+�U�Ahr{���:
/N0�����u�
L�;��m��e�?������`# �&�������HP�����`�y����[�=3On��z��(P;�i�v�<{v����rB�KO�WD��I3]�u�g��1��9��4��4�@��S��M�^*��3,�ja1��Mn)�NATQ�H�Q��A�)b�}�u�}PP��j��2i_�H�MJ����i�������&����2�2'[���)���5�~SE���T�!l��}���7��,�/��l�4�WS���
��$�,������69U���J�+�u����?�����������t�@��V��F�w`��:�<0�)!:y������x������_��|u9�K&�����wj�y	h��^�-����\����������������j��������k��8��/�/DT_�_�m>���;�B9�����,����)������u�0<�r�����0�u����N�G��c���B[���x���Z������W�m��3��{c���T��q?�1���O������V���r�Yo������H{
;{�C���t�	U5�d'a��=�POQy�����Z�����~�a��1/������dd@Y��������Y���vQ�����`�����/z�_��4������#�����B�/�>i10��q���n&q���j2#�4�yVs�Bg��b�pQ�I	���)��!�AU�-�~AhF���n�o�Y��W���L��l���,���*V�+�T,=d����Tm���bm����:
C�D��}�i$cd^�{������W<���[�T[�O^
BC����i�]������0����+%������`M:<�7�(�l�7sjy^\��9#�.u8�K����6\Y�)�c-	���Q�)i���Q�����|?�t��"!�=O	o��"&(������VS��k���-��yU�T�EZx[������T��

�|[X��Q(WexW�w����zx��������^��D���j��&6��>
�C�1���(����YX�RD�����m��������[�����Y�E&A_��t��d;�)�������w.�����ar&�����s.���l��v�_R����@��bD� �b���$�%/�@P��I��!����0�UJ�9�=��[DC�a�7�7P���m��0������MA�W��uF�B�hvc�;S�Ap(���WYU/|��I~��,��8����=�����e#��A�xAr�>��D;3qW�������{�59������Tp7����5���V5������A�-�K�Z^D
����w�aR[>����Oy��7|b��2�u�mQ���j�u[���^�o�����,����������"����o����p�@OZ^0�\Y��X{Ts����7������������c������
yV�g
�����`��7,��\����+���NG}+�CP7;c�%j�,oa�'������$�`:�&�7�X��?X�7��O��mevj�Y7�������U44-s=���t1��Q�L6�@�-
��d�C��p�'u4���i�(����a`����Pt���}�S�	L0�U`N���^����u����C��h�.,����C�b#�������J�#>�����D%��i��cO���i��/�v������������l���V6y1���n�}�����e�hC���]�@�������L��~&����n,4�'<*�c���+K������w���!t��"�����=n��Y��=��	r����~�2�_��;0{�19B���.8�4��{y-�X�l>�/�j}^?#[��D��,e�-��_�77������N���i4���%����i�l��:8�Nr�����,�?8�d����Lc!�0�>�Y�/�����Y����K���!�'hP�"��"&@���4������	Vq���d �{/�O<c����2���5�[���MF�����{����~�aw03�,����;��7��Eh�Q�i�s��+c�v$�����u<K��3������N��f���u������b�n�/��3V3j?	$@Rx�����,�����h��jw��_�.(��{���9|�BR��Ry���Y����
Ft���um�����#����o�wi�,�^M�Qe������G��d*AiL�9����v��k�6t���n�b�60�;���2���(��������;C����Z��;Hb�N��QF����Nh,iLG3�O����F�'
�����P��,��Y�����\K�rC�G�8,NFrY�-����)H^�>�f���-�L��@�|�1.q,������Kw�����K0�\�+R���f�s�1��bR
��C3�8�e�������g�UB����y)��f�����{������i�
.1lc����*e@�l��(�7oH5%,��]v)�����A�IU��K)�Q���Q���[�������>b�����~��#�2.�	��K_&�����U�Rw����+aJD�U����o$~H�`��P�^������AI���^p"��|��U����8Ro����JT����<��E`�F��!��o��7�����q�<�����t�������1��bmwV ^������+
�2��������Z��� ��"W}	���n��~��q���vZ�9���B�b]X�v�������q�^��CWu�s�*`�����4���?2v�5����������b�^�����������k/��������5q#�:�~�����]<��C��$����V8�tK�����@k��'v%d�Xn)h�o����R	cb���.��_eK��G���J��]��f��\!��7�@M%~n3R��+�n�h�x�����m���$����T�s��>��0%�O�5�3 b���i�{}v��5OqVw����L����������w���0.`�|/��@�F�Cp/YK)]d�I?����uG�(�EwDe�\��%�����"��Zz�
gZ�gnS������@U��]��o��Q�p��.�\l��j/����o��
��9w����u�0X�M� �z����l Sn0�YE�����G�7k��%��n�����.T���(�,�����QE6���HI|aP�UC�9�"�X��e�h.!:��X��.nru�84�������):��X*�6q`���.��H��OBH708U����(<��{@�
__K&��:v�����b����
�����~�9<�i��]�a�����Z��kw��-��0��u|q\�%��d�L�B���	UM��{���Z�#_�x�$�u�����~A&��s�k�_�3��~����Ic��i{����/����!��+"O�x��r��e�Ya����3v_)�<.�g����h���Xo�[��3q�@�0�����@C{.��
�KJ�;U�@\�0%�����H+r�Lm+��+Ph���
}m_[L�.<��-?�1�/1VB�J	sN�L#�9,����!+M����|4e���!
�6(���U��������I�.�������q���l)�#����
b�/�?������R��_�X1����������������h-�+|kQ��Uj��Xf�P-^�����!Q	9V�Q#�O'K����/l�q�_��gFYpC�|Q�EiE�}*��%�{�W������_.��2�YN7�w�i�������Q�o�):<3��;X�K��}��`�N&�$G�Fj���ZQ�L�����V��w��	�����c���V�Z��������s`�#'|�������x�d�8�p��8DX�s�F�V����M��tz��TY�4\���|��}~{��+Yh��uG�#x!��49��r�"~��A8��Ve�D:���u!N %B�vQ���R<j�u�$a��^1��1���v�u�E��f�Q�������E�8�G//��<)+���^��Q
���X�<��KN��]~i��2+�nE����]H�-g	A�XT�Y����=������"����pZ�����l�N���yy������"v�8���$�M�r���WV���Y�m�uw��T6��k[n�Q�Y	�
���k���9?��5�y���6�f��!4�T���r��t�8���m�[�\�k��P�����I?���J]Q��t����o@����1AS�"�\�Eu+*�k��^��J�m�� �it�n�������������$�w�4�ie,&�BM��6.���e*���������4�,�U�zV���%���r^��W�����^��$Luu�(��00��xh�-���kA����y�M�pik���Y�JZ�y�	��e�n��[/�V�m����.�V�m+�x���������#�<��V �z�
���
r�������V����w��3cAx��9��\�@�E7�|a x0W�WF,�I8��e�����Nw�6tz=x�t�-�;�r[d`�1�:��9�����-�yDL�$�~��IH�%0���uT�W�r^32��Viobn����&��W�g�+[���h�8�E��|/����%�5&]a���������?����)��p;�`���V�[���������y��
8:/���m-_��H	�9�0\�K�x�`(\��!�A�V�����������I�������66G���OW����W~�z����D�K#�X�~a>�dKm�m��4�������ms��u�}�
��HC�m]
�?"�vr��t��S�����������TB���&�����x||��8���]��a�1���"a������[d�����[�x��g(Vg��^�������&���z��5�������G}���p49|G��/F�'��{)
��	�G��!0Wm���|I�*
>��_~�,o]�Y5a������,(��+�X�nL���py�:Q5��Dc�K�D�{���&�������rL������=�_����w2V�C���Zq��z���j���jFoI�r���<����;�5��Y�����I���Yh���2���n\5�����g�,c�x�i���;�� �w��B�A���(�L5s���ct�\�����|�,[)g�G����*�yL�����6eme��b���P��r��b�D�~�����^�SY����\��
�<��T+��t��x}9Vhd�V�������Z��/���D(b���������������3��):�K[
8.�~�Pt$�	������E��)�D�(+���\�����������!K����<�5;�����_��b5�g!�x3�7��C�	�m���l�E@�'� Wt�aN��_�{_�I�:���|�������0 6�?u+r��	B�3&���46�n��i��E�b-�@���c�CX}�Y/��&��n�k�������;X��`�\3����J<w��4���}�T���3J7"�T&1��Q�00��p��@7 �����ik��5������b,<����!Nf�8B8�%�'wK\��3�����m�O����wF��)~�rF/<�0%8�M{�u��,��Y�t�:k��w�f�i�i>�U�����W����ja���#���xI�!��{v!G�����-��`�/����������U`�����'q)7q��u�nS��x	,]�z�V�AD�9*�������Ji����)�z��c��\$h��`�T�����X�����I
Mj�'����S3���fa^'d��^�E���sh��7��
2�X���������V���$T� �Jc������N,�H[�����<s�\�@��rN(�	����m��l����/�be:TUa�$��F4�8���>'�C1�=���!c���"��1���:���	*"
L�?(V��1.�d������c����� D��
������1������!���U�<���h��::�����@o��q�O)6����_�����x9?w���v���F��%e$�u�����+O�Tj��/�YIY�.=�\�$
�������a{�:�/���q�%���7�0�3��js�P:���-yX��K��}�'�U���I0)�`p�c�>�2�����{��"��X�
\�������x���A>���iX�|��A���u�h}���l���v�<�e	��Hds2%��m�~�����~��[�.k�eqIM��:�n	�,-���sz���:�g:�rE'�r��lNea[�3��w�F�Y�z��m.���c.���E�)����R������GS�ox��U��4I+������6($����*	L�e��X��x�~q���%�?d������C(Opc�j���Yc��?��X)�l�����ibe$b��Y{���Zzn����4�����@�*?���� [����U����%�Y	\����N�������5H��_s_a��KQ���������r;,0��^�	L��0ihu^��k��G�a�z��yki|�82L���Q,�����=���	�	}���L9�������"	���f	>���,#�9�k�:�Xc����&���f�����\<�P�� ?�
6"B�6#Ee��
A*;�������:q*?�vIT�q�M�������`��b�6�ZUX���rk�����)�{��%X����r���p>���ooHb��tm|�"��B�.��b��s���/��c��y��������aO��\����6~76[���BIx��RTei�D�8g��Y,��E~t6������H����+�i�	�XO<��,�#L}q�}@]0�������
��M_f	`��BL����R�(�[���K(�)>�0����mp��%�/c�.\�L(�-)H��`�Q�������?�u-�j��i����w4k�M{'t��D[=%�g8��P}�k��_�x���u�s.��$5�q6�<�^�FH���
�kfr
�i��>lMW�G�p�!����)h��E�=p[����v8jB���iT��1��������`��W���bl7\�o'��5���M~������N�l��c�co:���_&F*v��~h��D���%��r[]��d�����L6lj7n���&�U��B���v��3��c����j�z��\�E�����"_a�~l����Ud��E���6�x�(uO��)�Fl��6���S�s�X�f�)�,Z��X��}	�
����5,!��J�n��0�M�O?�b�b�Y�1��f���^��k����n
���YXO��RE�|���pO��R�[d��Y&���������YaM��d�8��d����q�q�KJF�M����	%
�������P�T
�!8FV��K@��#a��'��x�x�6R������=i�r;u��y����o -�����x��nK
��1�-��=$)�d!.���E��[:��i[��u;�k�c�p;�m�>pu"�k�-0���y�5�����������D>z�%���/�qR7@��l�����~������mv������y3��&�3S������r����V����I��3�mY<vu��v�m�&J%W�1��X�z*���4����a�Hy����y}�#��60<���#n�6�������U��)_d;*����J���h6����h�LyW@����y,����p<���*����'���V-�+xf^���E�E�6ut�E~�xt�:�+�&��W
�d������Q�����f���Sw+��$���4$�K������2 �10������Pz�W]����d5�C0��h�L~���<cs�d�0�:���O-�'�=��G�M2$��
������%J�rO�������4l����������r��~���Q����;��o�'�7m��^���������>��� \�C7�S��9�z�������C������K��9oV�z�uut8:�8z��� �9�R��]0�4�/�l)s�4�<�o�)�w�t����}��q���;���Y�3)���A'#Xd���-6����3*�yd��*"�PFq�bY��.��*]X��13e���(����f$�����K��9Ir����-�b/�L�#����������%V	�{y@x�N��T�j�D�=���h�6��L�$�x�By ����������+{:�p1�w�^0��d>���;g^��Co���M}R�8w7>
�]+2P�9���_��?�I�ZCPP����	�����2f��0��%r�,+�s	\6�H%��g�E�h%!&�!�3f����_��>�fd��c?���VA��.Y�Cs6t_/`���m!���F�����R�,�����/�n(7�S��&3��)!kL�L���������{
���������GoF{��S1yY�E_�;�#)&�cw�+�~7��h�9V�����q(������X� ��*�s	�\!�gX�q�`��%�
�s�\�8
�4��y2d�['u�'
�$oa 
��5K8�7	k���]���������g;\������"��PT��0�ZF����T��S)�Tl�g,��j�`���?S9?�F~\��������G���/�T��fNBw�G��+F1�.�����?��s���S��+$���1���~�����"��n
�j8:w<7P����?N^���'���>�H`3.O���&'<���x������U��~(�����l�e��X��EG�*1���(I�+��-O�,�3���s����J�5���#��e2q��
�{�y�@���-���s���
���#!l��c@g�L�f^S��i�QjI�,3L��,	^%*k�q��0|X�J#�����U����K6#��]��|��v�r-
��AIMMN�����?�>��>�����
7t���h�8}��4���<_GCt&�w�:������+Q�������pDb��+��2A���'�_��_,\G��Ot����W�^&�t+��:G'����q�S
���hI�lV%u<)�=�~�g�[�-�8l���.���!3�J�� F���U[�iI*p���:��	�7��r�"�1
7�c=������=���=�#)��)�d�'�&�GlE�/&��;��5�an�"�� �a���[
������@�W�}��/�E��a.���u�6��������L��F������#t��3v<��y�g�nh`��V"�Cf�Er���e�?���J"=�>&��HE�/�(�05�>�Y_�Vt"�+�N�=?���)-4��f�MM���W�
!��k�I�d
��`g89 j������x!���6fbH��>qN�k�+�>��_~CAU�L8�p��Xn}=��~�9����]��:)�dW#v%$�-F�w������S�t��|�5�E8y?��{���u��QX�`|r���&�?����z�ug����3������4zxr�`/C4�
"������k��K;}���y�	E.}�,8{8�l�����c|�:���W��������!��s�������p&�Ts��L�g�������[tt�cJ�I1aBR:������p��Nt~�pB��'���c���vY�o�]��{���_��gd�����l����������bt��P���w�*W@0H�����[Z�����qxvrt�����F���5�(j�����k�7�'6Cl������&u���74�jJ�C��@��F��S�B��)��d�L!g��El �x�V�su�${��9��A���;5\�zC�58�.|)ww$����^t�-�����K�]��;Q��02�����	��xH2\�rk���S���{D�j�����Z^���"@����x��h�C�%��h��Z�������
�������vP�N~N��z����)�y%��s1�H���E>���]����"�|2;�A�����|vqUf�H�\t�����h��+�=�x��Y��l�����/���+���3j���1/#l�Y��G��}���R+����>�/���d�������%-.����2)&[n�n![�������e�s�]��Bx"m����(��b�q@
����y��z�<�[�C��S�%#�����b�L.}8��Ql���$9��E*���s����M� ��p	��<E��mJ�����L����J��i^�3���c$�&@��	r�P�,�'�K��#��`����N���}���J+�X)<�9(�J7;L<{W|^��L����F{t�VR�><:���Oh�������(W\��1U�����M�~���nG������W*�9�{�L�kg	��l�\�(������p��\����{�������c�+�-<�h��N����6�EYsi��n�mLDJ�7#6\���`9���LtJ��R��w'��U.FVS�b���Y�X�O�yL4�'����%�Uc
��vb�uZ>p��^Jy�P��g���/S�6'@1�����;�4��9}V�A��@�S��~���6Q:��o!�"���B��)�.��Y��9m��3O��~q&��RW��n�1[�
pz�`�+	� ������TZ#R��a������o���������cq�����&Iy(���������b�6�����(�I�yI���Vi��!*x���~�c����zE
���x�|�G����k��������0R��H����B=���|}�/�����%���P^;6Lg�
���=��L��!An�
�;����!����Z�0"�o������P��;�WS�F�~�my��0Y*K(��z�3����k�7����JK�8�i�q�e�z���{�i����u>�+Dwe��[�6�~��03y���N�	�����'���Au�D	"��NT���L)b�`�����u
�qt]����s�>�k�4+�""T���t��>lCT����5)|����y�6����'�yr�c�0��������p���7
�yZ���pO�5�������� c6*6�+1���H�x1z�*������KKT��4/!������~Tg�~Dvw>7lc��������*7�Q��/o���.�8��p5Cd�FDa��[N��D_iqt8�" �(P�P���G�**]9�]��$��1�7�����0�H�B���4A
*�M��0/)r��aD��N"/?�<,2��IXi/biL	����.�p��&�F���/Y�����C+�0���/��K�������vN���I$�w^���7��FuJ1�lOD�&����
�[i����?��
s�$�F�����@:3������<`
���[�%`H:�{:Cg��]�}�j�L��r~-t8���zg6�p���y���y�G����}z���h�F�t���v�G�wd��<���x$>cM,i�7;kj�Bg51����`x�`���:�1e�����6#�^
��=��ja����gz�-�^�=jU���Q��������3���h���H
G��	��{�B1���2���% ��� �	OJ4�g�9���c�S�Y��z�t�#��W��5_KKm���B�{&#b������
���
4��o�^y}{�:Z\�;W�~0q8g0����p5��������^����k�I��
�(��x��/�
��<VW.�����/��N&u�N�o��7�CI�r>g+r����l����>�����x��GO,� ���4q��o0#�����������jzw3��k�0d)��N�,P��<#�o������"���$=��J����^�e`�I2ubp��N�B���,��P�����������P�@����^�:.��vZ����|�a��y���kH�`�t�����KOu���l���~!�t+N!"�a�
/&��x~M����t��������$��-m�u����a)Y@��������	��h�|m��*�U��m�M������d���)�*OQu��T��Td
��<���
��$�~e)�����5���bSVT�:)iV�MK��c<f��N���/��1�c��������,��OL�c�J�A��{�O���f�{j����n#������c���{�J�� ����~��#4@|��D8}�����[`jl
��e��f�G�eJ(��%m�D�����l�7!���b��.�z����{��f������
']����	;N!�}o6�J�\���!�]�<��6Di"j���lGe\�[ZP������I<�;^����$�S�
Q�U^N�7��gJ�r&�&>�D2�e�.�0��������������{��Jd�}�V����BA�����.F��Ez-���PC��{��|U���v�^��]iD�;����*�0��S�|D���w���G'�G��Q~%4.��v�w�$��8�����t��x;s��|�7����
�7c����
1�_s�v����&��g�CuF~�����/s���!���b��kx8*���Pehi�~��^qd���H����"������2�q����z.|��q!�����
d
#���5��@�(
�q<��!�G����y���@�(�����qi���Ma�0���D
O��b��P��� 
��i���0��)%�%����1���*)�	~��0��N�
��-�N�|N�uM�f+-�����ZUE2NZof	T.��tPi��#�C�r��.20K�Uk�?��@Vh�[|��2�J���"�	�I��Pj���Mv+�]�mxW�R1�y*�E
Q��1a.�BcW5-k��=��F�d ���f�]q_jnR���J�K�.x����p��������d��I\��U��*���Y����.V��Q�,�B8���By�ih)�h����1��K l��p�c��T |k
Y��9�|!8���r���������uZ�>Z���D1�b@U��$�0�)��r���tD19����$	P���]/#�k@���!��L��Hi�)3}ep��b=>���r���3���O�=���������������`��.�{EX���8�{U� /!���\~+��p����0��}���N���+bO������|���B�Hx���]	��Z3l0U����K�����g^�OZGA?Um��M��S��O5��Mt�����L�����<tc)�������Zf87{�j�S�=�Zm�'�0Z���Q�J=t�%x��3_��r=c#X&����Q��������V�>�2p�3�u���3�U����f��7���?c�Ix��2v��|��L-c��D)6xr!L$�F�'�QL�A�	���D�i�������]yC�����aS3��3�q-���X�
8Aw=?.���X�c�����(��g�D.�O�Z�k��}���hn��PZ�=�C�1�S���j�*��AUC�b�wsc�����X��aa�+��[J�Ame��r�����n�L��x�`|����_�hc�Db6�_�{ �	��\���@>cw ��r���=,����A%D��e�y\l(�}��Xo�z�m6y�
�tg����
I��O&�u��������C��8�*�c���MwO�N���IX������v��`]�����S��)�`���|��4���)�L�#K!��c�s/�$n�����������#~��u�m-������|r�
�VJ�L���sU�O��0��)n����l������J�Iq�7���������LoOy�7�9�#o����h�+�bO�Q9��'�x�In�\��G2��u��,&B����NC�H���v�'D��[7���v{�
�����!��m,6�62q��?e@+:*���(��i��nWe%"���O	��C����n&j��;L� f��`������+C��`�
�:S��uM�}��
�D���f��I99
�P�6�P��[��~�A�j��b���69X����,��v[�A��meq��sE�<�/K��t��\�4.���0��Um�x�, (��|�dtw����ro�w:JX~����wOZ��*����:�^J�W��!LK�������e�-���?�E8�A�3�e���"Q2m��_�V�m
0%z�7���7��������u[m���#�<q^J1�e������`fE�FP��(���1��-	�����R4�3R8�'b�����vk�SY���n���.�'�f���e���k6�V��OW=�i$��Q��T;�%k��lzB��7�R�����t\d���5��[�J�N�m���k�q��8�����0+N��	�[o�*[��[�!P�p�e���Gc����B�rM�%Y��Z`Y�u���&�Opm<�h�35�"�x�}�����������<�v�UM�tY�����9s����[�w%-�\�mOp	5�%��a�������D���J�H^�B'$�+�.5���.�z��������������MMZy���^���V�}2�G��b�	+WJL��+v��D�M��~�id�;6J]N�a�*����M���Fd��p�y��������gK6����m��B�g0�U��)n�q��F|+R�d0\����e����������ME���7�5&m��A��x�
�3[Gb.V=p�e>H�$z��yd�i�{n�1���x[P1=@���:�F;������/>L��K��\S��6[�GS0mg��S���������N�l�M�.�~�����]r���LY��������e����V25�
�x4$�������HzB$���y���)���*p���u�z���g��z	�B����!
���,N�������N�����
����}`'����J>���L��@9�fB�����7�Wj5�n���2+���P�>p[�N��L]y%I6�*A�4����\1�p[���^�&�%=�����#1��8?��1���I��8\y���`9Mz�"t��V+�>�Bt{�ra���x�:� N�c�o:�\��y,R������i��{���kqC`�K�r����+����F����.������Mwx6>u��o����U1[����{v�hq*]�q
��t	���������oJ����K) ��Zn���h�����0�+,�Dj���4�?��L,HVQ�^=l}y��^�'�jY�S�!M���������}�:!��w����qy�C������Sov�<�����[������Q���K�1A��G:�/R�sS�YI��9&�0�P���HPc�.0)ls���}<Q'k��H����t;�P��24���gx'�L5�_�z�JQ�q�]�����q����]��������{�vOk�����u����a���xb�5y#������P�������0!__����hX�K��SD��aq�t��O�n���k��D����+�������f���[n_����qc��)H��(&#��dW48"���n2M��#x��LZ�a)>�������f��Z��������}�|���P�!���g��������JxE����������)%���*��&���@��{��%�GJ�F�w�W��~����/{����V����Q���,�D���Xp	���o������Q�G<��������\�{��R�s���_a���k��s���J�j�K������>��u�)%?��X��cQ#��	�k��x�Hi�+t���B�h�q��e4��~2�E)M�����<����h�#Z���q$&�?�z0���s��;Jc/����w���O��� +�;@6�W�M��x�����<�8/~]���G.�<H7MZk,O��W�P���'l���b��b����b��-c�����5Wb��1;`8c���\�&�/���8��l��m����p��@P��Xp���o��r����J�0:��tP���^�
�F��Z������5YCBR1I+��5P��9M!��Ur��8Ag6�S��d���R 
���G�~��3�����1�����|Q�����]���Gi-�}�b���?�!��btbH�����]�%ni���C�-���8U���\K���������A21[�\~b��w.n|g�~]�,��Cf����Bl>O��R�O=
Z���`k��{��^�|���x��(���@�\a�?�0g,m�+�r���;�j=�@��4d4�����=���57;:�>����2C��9\.���SE5����9�U8����&`uf���eXTe"o)��D4Oy�1��G�5���h���p�������z2[�.k�o&���P*����s��G��@�"������T>2H��w�I���"�*"K
��s/���z�����[2md�����]��e��%/���-}���BZ�����$U��rbx�r&*R���<�X��K_�I�
������0N10w���m��n5��Z��i��-I'a�ySJ���`�K�����R��y��j���
��5���VN]�*��4?�l��
)�z����g��Xv��2�fl^R��'��j�p��u���;3����Y��M<�i?��Y�z^zs���'�6;��}�/}�,�U$���m�F���K��@����=3�,�Cf���b!�����3f�r!��\,1���s�$���h�I�1$]��(V��T�K`.��9�����!J��/��+���=�y�>�c��`Q��dP�v���<�:-���l�Z8C R?TfD�>/A�>���,)���\�AK��mR��v�J�6��C����L=��
8���TUW)w�.�0�?�����4��V����4��!�f����^K���>���\���~��������V��S�b���'����������.��6Y��*��(^�J~lg�_>����|Y�1��v�H��~l�)���Y�~���[����9�.���}�Tx">GY
��w���O6y���<mQ����bb3=�"���#�PA��t�3�����oko\>e�}��i�yvE"U`Rg��������1#�2!����s��[�$Mf)����$/L���,*�{z\P~�
*���>�����z�d%)����(m�m��v�o�(t�k��
�������3���N��[��)��#�m��&4��FA~U�A~R�F~��;�o�%��p�w����`�\�al�Dm�hd%B�����l��`�	/m(l�����F��ZA�c���w�7
�t��+�U��}�=�4���!ML>�Hg�z;�������dF5@�S��^fU��[4�(Q�N�^zF��������BbI�=}��adbAx�\	��A����������L��d}��A�pY8Q����4(�/��j��4������P��J�2h��h����l������:�*I���+M���+M�l�+��U�`�IXi�C��P����g"P|Z���*Z�����	���#�������n������@7�<���G��O�W�,�M
�Y�d�t�����l�i�������o'8p=�w���z�%�|�9�p�(��u/`!n��h�16�#������������%|��o��!�'Yg�\��7����g����+��I����iH������
��)�U�f�*rWP�I>yP�qi�l�����bp����c��x�
�����\�{�C�&��+��F�����o�-7�R�v�EB�<|��l�y��@� ^i��LO�/."p@��N$�0`G�w������/��>�:�������;5����j��G������4au���q�W)�F��@q�z��
*0�(�����7�����4j�g����!������K6�����
�}
x�O�������u�-1\�,���umtv6>s��qJ)�0����d�95\����z��`������������E�7({�`o�������j�@y��i���c�*����8M�S"�H�]���f�v9���u�Q�\���]!�H���H"�&'xH��P%xH���A�����Pp6A�Wd���d�@�To��X�?{��d�fx�f��(�3�i�!��HDJ&uS&y[��R��5&�{�/�nN��F���M�#�v�E��M�)+���cn��L-�l�7q)�tZ&��E��Ei�{����5��'��f��� ��wa1m?[}�Lr�L�y�d�l��8b���L[�lc���ig��I*��8}�m�\aqw�G.zPo�6��PRQ�7�L��e�~�����������I��r������^��g��&�c��}z2��B*�(��l*��:�*
�`�V��a^
��@D���>U���e��h�g��Swy`��+���(�/�Fc���8*��
��jJ'[��j�x	�,+=r�)6����v���������Y�����/,��^���{��V��8���C�m�f��_�������b�^�T�`�����Z/Q�bd1��2t���N�-����n��N��vz��[B��@h;���L�]������Tx��
��hB��r�(e$qtr8�{�B��Fe�]�+~'{L?^��s�d$p���{	����o�u�B(��
���}��o�0������;��K�!�P��r�vY|��G�f�B���E4Pb+�L����{lJ�-�	zO������p�
���V��ps��)�IX���	(���fy�|�b�w�!5���Y� yo��������"��f�������O��
@��c���t���1+�Y��]�j����,37E�n��X_c�P�-�!0�BF�(�A����!�^�����F�4*R�Ak��)��w��F~��}b�k�c']����
��W��F���0\<����n�JJ�U+oyL���"�Y�����{�)G@>,g��s��<��'��P�����y��X��x n#H��nu�������D��fE+��,RS8^`���z��x����lx1��9����*������>����p��Of�����?X����\P�V<[,i8�������4�B��-k�vdi�;��F�ml��x�y�'��+��~S��{s|J[�I(Q��}���Q�����z�������!�'8�!Z��p)���0��.�}70�c/e����Mn6��_�wD����)�Y�n�����L����G�?m�"��n����{����C�?������R�&�@4M�mnI
���k�%�V�U�����C-�NnB�*MM6��8k�^��Frh�:y{��~6���?8��S
<�k���B"��h��v��p���-�����iC�N�����	�L9��� �ip66a��^��W&�I��>�O8I�W�8]fb6�w���-����9<W.<���_�p��Fs�6y���6���?T���6�r�aJ����1bw��v�Q�:�ray��m��V���]]�Rj�	X�y�l`IN.�Oo.��������L���z��0�4���V��:k:�o������l��-~�-��w^t����x�|��]�Z����=��`+u��������� ���j�kY?��J���g�4e���I�2	�9�&���v�mu����g[p�Z�:�K��R��������5���3��5l��}����S4��7R��"u<�D�"dN��S����u�vj��;�Jw�rVn��e�	�X�����-�
	��<��V��Of>)��`g�E0�K����m�*��N&9�d�q�����n�K����,L���f�R��=�ZI$
�j���dUk_������l��V&=��.���K���!6��D������=c��Pe��z�D�!�$��v������[���_��s�t6>���Y'�~��))�|�m�����n�rZ��)��v�I�UPhV�(�z9���&��v���{�b��_��D�bi9{����%��J������{fb�M�l����-�B��
D�z6�ib,I-e��MER2���I$�r�"u6�����n��Z���j�� 1�k��M*iV%[F�
��mFo���B�>�
�.��]����4f,,�#���+V�	�Y`������&k����o���b�x\A�[�0��?~!T����?�������yr�lUx���{[J��6�*h6�&�j5�c/��v/(������G��]�U��1�3�I3�!�\��p��Y��5���|�(��:�	W�?y��O=�[�ak�sLHO���hos��h��a�tx0�x�G8�A�1��a�-����H#?���Z��������l���0�e��V���������+`��E{��^�"�6��.�oX�
}-�1�	��0��v�_l��3���%�/���g,�e
aq����@H2�s�����\�d�H��-�������Y���-NQ,��t�d�;N�b��0V�z0,z���m�D�J��l�=�Y�D����r��E��rc
T�q�*����V�m�
��H�^�%��|'�!��d]�*�����!�s��(�� �����P�rjT�t��5r�h�j">���9oF�g�.X��/�YDK�����l`�������f�������S��'7E�
��6���:�-���� tKE�����|���X� �?���S����^� �`��P�Z@�����.����7�E��/�8��Q��8u���VbY�E���A]��cJ���:=�%V���^s5�����`|8��;9�>:N91B�����b�U��!R���=I������'�S\Y�<u;@&��l���,����]�N�"^���;n�!_�7'Mb�w��gl��=���A�63�h���.\/��px1�vx>�d�L?��w.������!��H�Z��|�66���Q��]P�[�j�`+�i]?J4#f����lz���H�:t�+����|�L)����k�v�Z�Od�Nv�����i�%����@���@)�jY/�J*�2*��D�3^$�QF��dU���M����e���h��VK���mY�RGq���������E�f�E��������	��
����M��9|��������_���<FH��q>���`|<9�_��_4�~@��	�+eg�m��C}�a�Q����k����6���R�J��
�#�j=�����:�;�L
�%�R�b�_����OGg�G�������o���iE!�3)�|=^�;MN���w����� ������8�?c��/���LSjf}�����z�&d����J��g
���@��_������������*��v�P	��~S}G�$K�`�8���	������8!�I����dyp5�EO�yA114��6��o�p�6K����dJ&�Rzzh�,��������<�n0�<�P{�/���
��@�D��
)�O�����P�8zV�a��,;�(����>����J�.�E�*ava��������\npk�q��2Ow���mho�6W�dqf��e��j�=i7\��[���^6o�Y���"�9�<HVqzQ��M6����&�+�_�(���k�����k��z�H�������G���R��Z��6���.D5���~c'���Au2Y@��l'$�{�f
I�j(*|,����$z�c���8���,���M���:���������&���@4G�@������NkL����fK�C8�E�	���!��+�`q��E���>KvK���};��
��n@�ag
E��LC����G�=�Q�L
�+�@��D�<��Ai����^�Y���a�u���\��%�X<��8�����W�"o����\#H�`���n��.�>0&zvQ���E�U�h];��eXo���p1��X>����dK�����N�C�d?�?�/~����k?�l@?�����42�Wd�B���d^�p/�|L��F%�4����x�Z�"'�93X�SFd��)33w�sBT��@�DY6]s����XH�,p�b����%B�Q-��X���f,n����*���K@p�v
��4�8uj���@�K����+����������.�F������y�Bcb
eL�_R��s�����Km�����	�t�����n�,�q�0�LK�a�{/�k\E�eb��\�=��Xo/o������/��9a5�a�e��(�����yRh:�?ju0�nG��=��Jx?���������W
������ y�o���H��#-z>����f�DjKiP�6X*�u��U�-�D�<�/H�"d.�����8�����ps
;���]��p?��rPp{����a:oN���1�*�3��X�<%��P������5W[7����-�;h+0�#	��I��9�k�[V�19�]�(��eW�+�n��d��d����~	�
T�"}���S��_\��eb/��bf�P�Om�v��(�R���xx����'�c�������
 3��0��!Dq�4��D��l"�g
pv�
��nr����������'�W�p��&�8�:�b��[���'�K����\�]J[�}�M�;m�\;���jn���S�Z���4yDd�������/��pw(e&�ip��D&�3�W ��Fy6�,�k�,��m:�V�����u:�������g(�����w�t��
eK�y�/`��������@��+�bu��a3�C���p��`�/��"��{�,3���E�����N�7������[�v�)�����6��b]���3X~�.�d|q�R��=�q��Kgxx��O�/��G'<$$FM��4�x��f^I�KxR�e
Ls>w�g������J���J������c'�
�{��z������4���4���5��
l4<�B6\�{���,x��S8�&��t�a�{�i����C�B����-�2g<u����K�C	
.�'(h�m��;�O	������J��!��4<iM�]�@	���^�����^l�P����[7��=�7���������2�����;�2��Kr�Xbv���f����xu��;fD���g�Wv�U��)�!q^�g�:����Y_���7�A��-#8�!������+�`��X�^H�j4&��V^1����J���=����>T<������FR��������	n�I.�|��7O�|�e(��j*����8mr�R�����b��p�����p�z�t�S�U�#�&�zs����DYZ�����I�U�9t��W������
��P6��R���,�S+2�"~��jL����o��
O����Q-��'y7��	��9��B�;����
{��tCH0��J�",��x�f���_��q��������8�O�/
�[�p�y��g��n��Rw��l��9�!��J^^^���y�0���e�s�0���O�������^]w�'�yNt3�!I6Y���s��>#����Q�������e���Ea����0��������{�������������p�E����$X:Mx�+���,��M�/��dS��'q��q�ME�P������N��_M�B��#�9�Y(�tB�"�R|�����b{r�{��Mb2^��������Yx�}�]���dx���-HS�0�M����)j
���;H(�L�PwV}���z�DZV�����,5�W�� ,3���'�L#��UB�p���'_{��hVN���V��3��e�����$LW������
�/����������*�#�q�=3`���1�xo�"�3�c�qW������R*������a��/�w��r�k�L��D�(#��wC1x�e�����z������/�-6,�u�n������5�^���D��+T��r_�F���o�:]�6�D�#����N����f�uUCn�~Q��1Uj4��
�����H���k�BO���
*I&�W�%��`>c=c�,�	`��I��������M�����8�^��=�&�����C'R�P�:9�t�������2@�7h��V=��g�|�-��0�W��4]��1�Q��m������������=�j���'j�����]O����l�����$��V
+7���
b�18p��1]
��m��V���<(���QKcC��/'w�&�P��oh�s������,FE�fa��z�����K�GK4��xs��FY�c�^�����'%3��^����%r�p�)ht{nCP3�}���W��=�#�����7�^���9
��sN�g���$�fn\�������H��!Kq{ =��5+�G�X�<Ky��A�����7��D�5h�����8��?p�Gg�0���SCR�_�p�gecy�����?NJza�����f��S	�!�%?���sbB�`�>�MH*��(������~cc���V]�W22$8j��s#s��WaLj\�YW�^��>
�	C@V��19�-��3�O����\��_n����Oo���Z�������j�*"v%��u�����~��%T#��	��j~�B0>������G4��-��$Kd�2���>)f���["�1,����E����������IW�uTx�U��M���V)#�����z��+m'�����=�%<���J�I�h�mFU���
,lR�w��C���#h�:<
�4����2�Y�//M�����T4O8��e�i�[�V�m������wF�_he%`P�m�R�4�f�~����Im`��v����������,��<�m\���N�V���?<:@���OY��C��[���$��:_�s:�p��p;�^�`{����&m��N��v�M��Wf�^��h��ewI��[�oj�w���y��>��?���bl���a_�2��^���7����z�~������v=�Q����������*I��]R�a��m5pfK5�t�����6:��-��_sJEF ���sPFVX��TB(��gy��O�	T
�*>)��m.@����^���5V�q�����<]�B*�������!OK9���o�'<�)/!<��|������cJ6�=�Nd"���6�d����e�������D��k��^��-|
������m��^�h����#R���:��S
>[�n-�b+)��D�5!32�g�x-�%�+���<I���d��u��F��������A��n�3�:�����~g��-�#W'�%�l�w*qeF��������Y���%X���`�l���,m���6H�����w��v{���z��m$����]T���";���H�iG�g2���t����T�]�zs����.FoO��c�U���.J��|�Qo�\�g�/����6���)���Qo6a	f���#(4���h[{	KE��
��o��K��O��&����v6���l�K69�m*���/'LL{{�z�:�sgPQ�������*����o��y�F�d�>0=B�bM1-��9���"�B��t�QUP��TGl���[�5�Hn�>:�4����G0�*{��x��J��2��i�k��G���v�7�l��`������h��p���
#7\Z T�L�O����=�/tiD�����G���D�����l5�7S�b�R3E��T9�����5�m�t��d�������[�^V��+Y����z�"�/O\�:��^��B1�sGb�}0>y}������z���$H&uzu}�t�����!��`a�����0�?�|�y$�X�u[���v�7g%p��=
�Q���R�G�0�4D��{cx�m��5�H�e0�
��CF�5j�>g�"��M�&����m���������Q9�8��&�m��cn����^O�oJ76A�����c�6��Z�r����K�2.U|����`�!��6�n��re>.���o����Jb�B|�4d �cSNR	>!)�G9�%�!V*`�Q:�:F��/������!����]r`��1�Rs���m�HV�������d�}�UmO��)�f�HA
��R&mYp*�r������a���������I\:���H�!PH �P��g��1�r��}��m
�Klh��O9��3��&��n�-g����n�7H!�#X��^�0x���B{7�}{js7q���
�	T�AH�=76���m_��)�{�1�F1j_�l�I��)g�W��lj�������EN0���_"��e�����!x��;���
���-��,�\-����O+��!@���t�Zh�n���0a����c���LM����r+�}9�������?;f��,������.��X��e���Ft=��L����1������&�������j���g�x�^f��C���`��9�cL�\���	����n��5�IK@����n�kX��Dp@lp�P�5�������0f��e	���)G�����
��fi�����#���V�=>���a���!~�{JW���
zA�7��n��v;�>�.N�9�4�^���{���h�/�����S��\-�������m[L�����+�����n�aDK<$R����?��x{Mh�R)R��������r�j���7,|��S�_�%r ���;s�������\?�J
@���cJ�U���p0���5�e$���d�f���;��+n0�WR��� 	�d)�����#���Is�Y�����(���A���2�����S�}�DF�)p�:V�'��]�z���Qou����	i�{����f�D�'�]/�������8��<����L��'6N��[e�����5�Y;���1�������MCp�fSuEe����~v�H#&�|���0�i�]�������	�W���p;�~�
�Md����)t���#V�mR�$%�&���^&�i&�������������,O"�g�v�T�Vit��tUp��@�5�-
8!����\���`e�)�l�Va��b��*yM��i������C�~O�'�w�FH`�'��zq��*���~r����*�{��j�{��A���x/�{F�_	.�v
������s'\9�g�)�<ITD�_U��h}	��O��9�M���N^��g��_�N�3�>-j�z�R��/7k���|N�H��iC�I����,�Ceh����Z�Me5T���qG���#0U4��pZOc�6��(�-"�A�L�Z_	���x�a$��:D]�<sY?���E���>e,�R�D} ��������=L������2��&B��/�(���?��	���[��?�[3�
i��!1b�3U�M��x�k��^&s����w{n��'���-��92�����-M��n�[_�����1�6Z,�2��`>f6��xR6�����+rK�O��{�(���;�RA��B^��\��eQ"��i3o^S�ue"���������h��w
&���M�k����6���f��nC�F��"d�x��|t�������/0\L���<�0cj�	�b
��Ur��\+`<^�i��x� ���"[?mJ�nJ��N'��s��;�m5�z:�m��0����`��0����RU<V��T����w��{�������:���,���"/ �������?pDP�3�G$�>��O^���>��L����j�V����h�/4��'\����b�C�������\e��}�&u��������+��x��0V�F�-����k���.6a����X�`����L�8��s-�"�����f�pSy�<���NB�9d����[��4a�#����L`k��k�R�V�++
(g�e��`r%!�at��;�U����f�TY�%'C���9��pw^`�ei����/F�U����>�N�^qq�r.�P��j"I%�8q��R�����Qj�s[m=1�4t!S�*�0aO���S��_��s�G@>���n!Y�L[AO��Z��
�r�g���L�K�g9%�369���D�����L�?�
����$��u����&o	��g��U$�J��
�M�E�
��d^�]`n@���v�L��������^q3��^sFgl�
y`��4��|E��;fO+�8H����.9�T��x����;���.�g�[���m���	|.�v�p]'h�B��P��0�L���C[�`�l��.�����h��������	���6��fge�l�x&�����H���2��x"��n��`�$�5�T�|-�����i����|�5��C*��G��Q�L(��u�7��lM'ycWXW���O��J��In\�
�����]9��x�Ab�����*!6�`����l���8/]�k5��N��R�e 0��un�Ypuo��W��Rl�������K`,���6;�t\b93��4��;���L��S�s��o�rNg���	�RRtZp��4�b�����p�N���Cc�=���c���I����1j'��&��[�<Zhl������Jt�������:
e�
�P�"����7]�o��d+���Q��g7��A.1�oa��}gN��M�P����e'���z��`�i����E�a�G���$a������r	 �@�p�j��d���F[Jq`�����YM~���o;��+oy���W���E�<����|����w��:|�6b��Q4\�!��������U8�����|�����S���3�=��
M"$�L��2f��g���IE���P���$,-Ty1����'m�z�Sl����.a��\���F�:H�i$��)������u��9�^@�����m3�D�������.`V
����W���#��
H�a;'M �-E8��v�p���U�/�O�6L��D���"X�9_/��*~27�ytt�#�l�^�[��7`F��%1��;o������U�tr��t��1�����`���T�*���[�8�I�/b�-�z <�����jJ�h~����;>����{`���bP����n�7q�����w=#��gO�B+&��s F���v�S�������;:��M�4�x��CX=
5��
551��|���] ��?�~|Z���6����3 �D
��M�w���P%��1N\��q�C_�tM�.��u�c��c��vk���J��7�@�m�e�~eGp���n)�Q~���,�CnkQ@W��[��Y�w�k�+�gf�4�}P�lG��
�N��6�i��&�YF�Z%J�b^�d|1�
�
�.3��I�#N��\��F�Kq��E�����7���f/I���#����
��)�'�
F+��B:�0���f��|�
z���2k�Q�c8
�q��o���a�s>w���g)��S��%��F����v�/#@�����q�aX��{�����V���#^�N��t�I7'��l
<����������9�a m���53w&���V��M������5\��	�=A��m[�� ���������M�V��G���Ux�'Tp=�+�.~@)���d��`�W�����1�"e4t���n������|h�5YIn������	py��G��C��-�������7�|�;�����3���
	�mK�Ka���I�����*i�/��� e�sp;S;$`�*2v�hb@�Rnk�V&\9)�+�3%�M%n�f��u�.��	���J�p`��J[@��dB���2���>gEqL�9=~�����G��Ug������t�]���T��'	pK�J�+<�W��r��K��Y�D���\���Z���L^������C>Po��_��v����	^�������n�.&@��/���,\<���^>�&&���n�{'�	��vN%3�
<��QSo��3�k�QX~0:>Fe��"�=b~��|�C�$����p��^S�q�E���+hC��l\���k�a�d��$����Z�>��	�#61&�Q�g���C���o;�8(������� ��&i���Wf��s	E��k�����brI���
��|a�3E�������^����i�R[�LAS��p*bg�g3��
�7fT�!O��!
{}���P�R��'��d���=�R��o����	�@�4&M]*>b��tx6�c�X�\.�W�g�,�����J6��
�4�`��w��"�,\	n��$C`Y��?���b�J��4�$�.J�?�*�R\Q�c����k%��e����=j#�~���?\�H32�9.����&f�n���W������[���w����q��-�]�Xm����b�"L���p�.Z��2h�]���kP}|:�u�a��b���3G�i���@�d5�{��
����4/�m������f�M��2���V�t9����	���Sm���|tvq~��bWR����JO�y�Q����>[�H�`	�\f�A����[sI�I�4��`��2,�BXG������B�m�����+?��6�����GU(�����C��D�CY�'{���B�R����xog�<�N�[���<�X����A��;x{�\����[AX�W�]������(&'x��%� �v�S����Y�o��c;?�2�,hz��`c1�l�mk�E��-���4p��������dyh��
O����wA4���2���a�zA�'�nZ������o$p�$)�t�
>s����@3��bmmT<a��m��Y�K��:���l\H��rg��[�t��=���L��y��ISv��+o=�O��R	[�rc�z�������������y����V�L(�Wx��Fs���(Z����sX�7����3J�5
>������?��y����K�����4�Y;��T��P�@���r���09L2TI���8T���i���O�FN�t��m��I$P�7�fC���=���t��/	Q��%����J
52�,�������|����������fu�UDZ����g���xF���hf�)8OF&�4C��9�,H��:��r��Y���������z�|V���W.w��6��������:L9:��[�#�H�������O#\�yg�����i������S�d������-�9�V��e���w���#;Km�����y����$�OJ��s..��P��\�//:
G'����c��}M\Xyp���&�����y�A��V�p���wr�EG��Y�����Q)��1yX������\Eq���m$�1��r �!��N������0>�1�^��,}����x��;	��*�5V�������O�'o���7��������
�n�9h�=a�������G��,@�����y�E��[�=P~u�R(�JG�/C[�Q�^p�\��QArej����
X��BX�*��p��m�=�]n����*)��U��,�Xr������"�"�Bd�O�2����C�����?�?x�f��* V��c����tvD����Yb�8�������6�*��{�=/��J����������O�T�"L�y1������,�����o��3���<^�x�@+���1U,k��J��2����	Ko�YW?k?��#�,"~T�5�n�����'�K�
W1
g�z�����m�\���������n���<�h�^�t8:8����������f���������s�<���1���@�z�@������o��(�<F�r�#3��8g���"�'��A2�p��]�W�Oq^^(��{�����l7��mk���b	�'�1�^���N��6�P���*����U�1R�^�������l�vt1:����{2�(�t����-����;|���V�s�l�)�����o�g��A~w>���V{���IeAG����cSfMD�c�w�4���w��+G�k�{s���Z��l[G7,m;����`��8������~�I�[��Uow�W�,���bx�?"�'��j���VV�5��q3:e�����y��[��$���~^����(�)����L'��H�D3L*��ic�w�0������/��)��z:�:��e���zG���tBs��F���/���N����.3��5�{������jZ�������;�������i��y`���<0R�X=��4�Z�r��������R������r;)��f*EH�EJ���.%�/������PZz_fyB�w%e�e�$���4�i��	��>)-�K	�a2I�.yKw��ve:R�0cqJ�V�C^d�d
��/N3�O�O�m�w����4	F���}���������:��������\���}�����&M��|�����hS�j������l�N���N9	��7�>K�yY��'���;�����>9�YV
��Zf��
�~���!�T���1U��V��9����ge��3����'nVS�����>m;x������>�,Tt�v7)|�p;o�82��R=�t0|����(��z6N����s3a{�Rq��"YJj)oM�$'�Fxu�������t�d��=�_���\�o�ib1�s�C�3���Z���QM��'X���)��u�i��8Q#s���$Xr,{�Us{�����������cwB�j��y_��Q��k�h�
rQ�$4�D/	
{0[�l d�&IC��OY��!�����pK�@�eN.]���K_��z���m[��R���PS��`�����8��-�,{��_��3}D�9�VPhoHq��
��1.E��������,Z��)��<k���>a�J.V��	�i��'S�A�q$ue���b=�����������p%U��������,(�*����:��G)��D�*W���n���M�1D��("�������$�l��

8��,��bV��U8.2�a����x[�����
.X�h
�c�a�a�g��aB�?�|��I��E.�}�����m1]����24�u���z��Y����O�2�!�2d���`hlL!���^�e����5&9�r�����_\�7{���}�t��j���D+��Cs�K��aT=h���.�����b�]�i�ar�:��MGy�������T��M��Iw�]�/&��~�n��=s$���u����yC�|������"�����<�nt�}�9R����������O��G?�<�j���S}��G{�"��`YD��*��J�W��w'G{7�v�������7�;:�M���P6���g����4���L6De������X�s�+�����U��b�����^<�Q���z9�hn��:3n��s�p�L��Zv-�����j�/e����"��S��'��}����9���hf%Fd�G�8Vk���K�r�����hxAK*�m�8�����5|��+^/��3G�%������v]j����-t,�j|��au$�z��7�p�d���D��D�c���h�0�K����L�&co������KG�Hq�Q��$����i�:�������U�1��&?l`h�F���s(�v�^|W�����S��Ke�)��}�H�`�����*`H�']���	�]I�4d����R��	��rt�ff������x��nN�)���[�����+�����K:�ss�+��Y�����pK��nO�*��:�]�9�;��^D��-q3:����p����	=��t$V/�W����EXo���X��v�*/
��g"K��"� +YUv�*��*'W��T��
�+?����&01�Md��t�f�S�`sAc�����eGH���M��6;��l�z���1�h2�B�Ig�����_8�o(>O�:���s']�P����$a$�4�p��d��{�`�o�����*�������@�	9�fLR�����PY�y������#v�P������8iF�����m0��f�I�����B��4M����2�]@&5���6���
#���P�
�.2z�?�z�e��<�Wa�����Ni�t-��@����f��P��w�������E���n
�(P�B�3���^�S
�����x��&'�� �?�:�?c���"���&�u��~[��!{��B���at�������������N0�}�<�������8�< �n��R�G��t`���gBW�e���D���g�z	���Hg��K�_�������_��9�A48��{�o�`|������'l%�;��/e:����O��+���<��>}P��7����g��N�+���aL������`�!���F&"�)}s<��6����p6������Aa
���2���A{rh:�|}�0������^���GV�iR��v
&�$�g�l��
�6��y�C�s>|7r����C�q~0<�������.���7�S��;x�9$�;����%D��_�;�`���������>��GH[y@3�`<<��H��cL��~���aV��`?�����g,P6�v����;;����#$�u2:������9��������w#�+w��?N�g�����t����P�������T�8����`n;>�^�������o��8�jG��<��
���_p~������������4�l�f
��$���0Y��3s::���m6�6M�{���ki0���r�|�iF����_���Ve��I���^e������gK�/����'|��u:n��0�'9P�o��t~T,,���U�������������p�;}&leP��D�}��g��16���������������s�	�B�7@����y����{���(.n)Z�?���[)d�#�X�	6n���PW����A_�g�������#�������������l�cM�V����.��FX�A����J�	�@0[X��D`,,����*������w��o�2��'?��D�Q�*�Q�������]X����?���,�6�e`����l��h��u��/��:x{8����>:NX�W�����Q+hDuD>�q��%je�2���h�H��`l00�����m����}^8	�F�����
���=8��3eQ�x<�,�G��������!��;,G,Q��4(�"x�����9Kj/R���W��c�u�,���zXIS8
��z�����u���\�������k4I��5�]�}tG��	��/���Uj��g�`��5�v%
�$C*�l
e�����`k*��-)p�u�T�1��P��C���.(d�u\*�7:�U|r0�����[b,7�hX`o2|��l�~�5������</�g�����R�slMea�^��S�|4<;��9��#uKn+T2������phx�S^�V�>/FoO�	�9]�Je:4vy�3z���x��E�F,�o�h��2c7�}4V�&��+��V���$�eEv��(��4�M�e�a�j�X���a����8�������`���>�	L������`Nw���e���;�8~����/�e����-gO�X�'/��z�x��g%���M�b�H�Z��&���2uk�m,�}�.U���N���Mn������-[%l
�����l�u y�����R��Y��V`kb�T�UMe����N.���d���2����4�Hu�A4,��iF��<�A�1������Ey�8���~<���,S���b?�g?$I�V`����%�F�s��gG1Yfk(�<&sO�5�s;x=|{t�Sv��
q+�`Y�l0�5g0��,�2XF��6:��m�1���>~O�g1zw��WY����V.��
I�2>I�R�pOf��D�9'eGZ�]dd�_S����(�j�:����.&���������X�*�����g
��������eZw��:Q�����6]y��������iG�%&��^O���_������_5��s�?����Q�9��5���Bu�����������0���${sl�d?#h�x��#�����
��#�����X�(�J����H�@Z�"�%8&��E��Oc��-�+��(e�����|�%^5c�k��5�������P�wf�7<�q���Vm�W;<:��x�dY����]��ZX����������g�� �n���/�����Bi�8��z5���`tyq.���l�:]�;P�����c������U�XB������O�)q�
��
?�'���A��z�h})��-����+�uta�RM�t(��� /�/R.9�4\�\��<����lH�����r��?�����C���*�E��if��\M����loY����)��0X��:���2(�� ���<O�������0����)����>�z���
�F���������:XDs�����
�q���6�c���1L�����p+��7�F����4<|�/Rss�P�8 �aFS����\%|���l���s�@�p�������dx|��>���+���8��s�|�o4�2W<+��q���}��-��Y���\�*dLt�������5�}��5{�������}�]�7�h�������f��TI��I�2K
�b�.�)��D�p�Q����T�����f�T�^a+�S�u�T� �Ql��
9� ��(�<V�����_Y�f�����f�e������������tRh�� &F&�����������2�0k!�-����L[�?T>;N|������
P�~rn��:�=@'x�KyL����[e`Vfd�����f�,&�����8K���^kk�t��4��b5�Mw�W�b`�E=��,�L�������{��\Q�~����(���5��"f��G��ZM������h�>�<D�����4x�:><�8�Oe&d����yp0��/�XSqS�]��0��'��'��'��'��'��'����
��,A�,�#A���M�O~��1E
���0���p��5`��X������_�h���(���!����#���w�W�A������������
ev�J��6D�I�4�r�V��i(���v�j����I"U���T��Hk�{M���
���
�;��V���������M��X��N��v=C/�s�G�{S?���g��+��K4�%���w���,LF�<< �:����A��I,�5���G��k�����"�l	�S�����*XE"���"����>�����iJ�v K���<�N�W��d�R��v�O�>�}T�A�Q�0,�|�D���>@
������8���������������Pt����7i��E�����i�-�6j���#�G����e�GB�3� f�Z,A�{����%_�h�{�6��@�b�@@������*�K��P���L�$�|��
������iuO������2�K��=mH<X���`�G��Z���������!p��cj�
K�*�44������F��$����z���H����Z3G�������G���}k8�e�����^������^����M��#��/�=dNP��'��D�5���h1]����4��{y���{��Vr!)�#�'�/8����� T/�&[�'I�5=,��R������^�*�E@�u�?���.E~�����o��#���$1���T��h�k|����!~�%�������y�q.��HC�����^�����
 1���A������{w����=�ur�Y"�4I������hx��_�.;�8�V�*�G��4M�!F	�T�� ���i�����QoSHE�|�m�Xj1���|M!��uRd2�/�M�B,�xm�������xa����}��O��$i�
)5d$~6�d�4�����yt������g�/|��N�G#�
�~c��y��je�I�WZt:����HR�3�|,f��e����,m������^_��k�yO�H�c�rm�L��}��(�����{xpt1��������W�a��jjT��,y>�1�{�8\eL��D��i�I�HbK8g�>�L����ok"�Sh��(Y��&$|�6�����l��<�Oi����LK:�m0�a���T�HMgG���|�H�OF<�f�m�����i;D3�	�wy^�@�]���>65�0����U�����t��>W���P�JSzjZ]arOC�&�p�m�����kn�#������Bs0�6����%AiS�5!c�(�W�����s`��?x+�)P^�
���S�v��Zp���6U�GZl�Pd����3��xiPo����K�F�F3���B�������t�@��=���!1p.���
���~��1��JXj��/X���"\C��~�-n�����C%�������7�Mib��Z�Ci�.��|�U��u��Jv�#�m���*W�-{��F��h|�y��i����D��z�H'��&!�Xi��]�P�D��������hW���.�0r��������.l�����T��F�3��&*�F/�f����M���+�aL�Xo���v6l�TM&��/�MS�/X�����a�n��������4�8G-��=o��k��T�0��lc�Sm���`E�i�,�(g��'7�	Uu�=����L��a�*��r����<
��IF~�p�4�nc�2i0���G���w��"
��'{Q6B_	��Q��Xy�l��!����EIL�
��7
��S)p5��Y�]M���r�o���T9��C�Gw���cj�O�)<�M����`��K��2� ����Y��E��z��r+/�E�8<;9:y�����w��������y�?�I��_z{����XC���B���F���Y���'aL9����U&br�\�b����aE��
��g�
�v��6�������8���1��>`.�
�s������N>yHK�u��4��Gm}d�i�W�$���(��x|�rM�.���%��_Ve{=���n�����A�M����_��|z1&,�
���^��"9b|�%�Vy�C���3���K���w�*�����==s+ >e�*�K5����^���,_�w+x��?���3g�XO[hP�id[aF�Ws/���d$����)S��g����4��z���<�Pz�����������]�+gw|r�����L���R���SQm�(�^~���
2���(��������0{�)�_�$�����-,��T��)�Km���������o������r,�}��R������r��$=��JH��������V�����s���Vk���}fQ�f��0/PvB��&�t�����
W���P��p��-�B���!��9����4�#��$��o���(�nx}�l�X+��:��xH$����},��H@=E��%q�$�v#5��'p���y�l��0��$<��p�������\����.H���	��s����tP��-�q�S���S%�M8��
�,|<��J���nI�x�h����'��1��G��QJD�L��'7>�&��+���U�~}Pt/ ��o�������`��~�nU�S��Y�w���}���W�8Z
l�+�'������Fr��U�KLY��<��6�"^/#�"����{�[��<�,03\�<;.kF����j*0�v�H+Q�]iZ��M,f. ���?�7_;�z�^�\%Yv�v���v�G���s��r�;����	 9�J\�
9x��������[��������������%��t�
�\z�32{��C����lx��(`�B��u���:g��P	_~�$�uN����'�N����{	d�J���o��t)K�����xI�����o�I�56�)���W����N�4��
��D�u[�������RM��w�R���5c��RK����X+�B��U�$���hd����Gf-���WN���'x�B�"f{�����0hE��<�S!����L���m��~���
%%Dd�������O�������w�!���Y��D��pp�E�j�^��(`��B��t������X��T!X�x]{GW��q�����k�u�L�t�����VAA��Ux����Nh7^��f��T|EG����K/f'�-XJ1�3�r^��PB�G� y�;����\��s=�Kr~�y�'�����p��4���I��$�"O�_e������6��R���;MSm�`��P��L������<�
d��K�|�o�V��q�_!j�����G��R�VOe��!��XUB�dl���~��x�g���_�'����N�;��I�?�e��[��5�*�"iU�50~d�y<2��$!�������9��oaW�>�A�����6��<IZ3���kh�Nf1�oK8�@��D�"�����$�����[O�����&�d*���v)��}���g�D��]]�c]R~k!��M�`H�6��+�t*������rF��{�sZ������5rv�#�)8yj�<Q<��e�X���"���9b��g�4�����]��4��]�$�-v���9gyt'����	��ee���}�}��?+@7���U1h�^x�Z?��u�u������g@����u�m�x��_ ��)`��o�F�m4��R�����6�[
n���������������=�����3� m��K�*�0�Rd1f
L�dfT03�Y�A��$������������&���*g:�)�����<�khF,��p�U�W����9g��]�K�C��w����2�������Iq~O0/�L��O�pA�������Y:w*�eG�;��w�l;w�hP ��x���I�<��+c�y��k=a���b������_�%��������'�d��d�.0�&zL����T
��x�@d����)��_�;#,� �����i��dO�

���Td�U�d�+>T�-�.�j$���5!��������������%r�(\�E�Q���W���/D�i8�)Bp�
B�������5��s�L�$E�@o`V@b�K�t�)n��4JU�zj��+)�A~�x�Y�����F�,��55m�>��������7�9(Zr��0Q��_s��N0�U���������V��Nd\�d�,���2]�~B�9�u���r}��9���=a���������:�#`Vg���v	��������+g5�R�'^�Z��l�D�X�`�W5�.��b��y�tz�/�����(��������W�+�X���j`r��F��_�9���Yhez�-�c����"@��f{�v&f�I�|-Q����
>�-��`�p1���n��P���s����_����9���>�_L6�3����R��(��.qx�����������h��
C��������O�'�F��k�Y�����X�Z��o�x�'�N�?�x����[�`�![j?EZ��5���p�����gIN�2��� �gy�6�Y�`��z�����yo�3��O2��Y�����~��&�$'�`�zj�t�r/c��>\���2���%�Nu��F$+��Z4_�."����E�]jA�?�?���'=#��bM�"5�\��E����~��4�P�yB6�����e��9��7!�����<>Ge�����Fu#�3����������D}��l�z�,�����$�2���g��#��sU��H�������������*u�������'c5Ve�^��b]Cx���d����	ib3��W�����r��Kf���<e�5:�K�!$u�d27S�F�s���S�J�,BG6��3>���k���E�Y��fv�LZ ]Gv��}&�S!�v`i��sK ��4w������#Q���z>?�.���8������v1���L�C�F^_�����+=a[�'H"���� ;���t��S.�z$S|MOk�{^_s)����|B	]����g*@�F�������R��D����v�^������������c��-e,/�jc���H�W�$��p�����.F)����31#e�b������s�}�Vb}��)��s/�y���z�6���b��%|_	�l���W��8�D��S�8?�<����_�ar-���fG�:�x/�~1���
�������dN��?����/��<��u����k#*���/2z���� ���`:3{7I�R<�)���D��n2WFg����
�xN����%3"'��Y-��= ���,�|>��Xnb�RX�|�=`��
��[�������4�J�I4(l=w�����bL��'��Gg����St�!Ap��_MnYHl.���Q�spLRVU��6k�������+�x��J���j�
�������L�M����/�o��������E'HF�t�`y���*Y7�6�<1��`vg#x����hB��5X�H?_��������u��z�E�K�t������`����EC�������\��E
y:�.#v����'{:�9���&�vK3�f^bJ�v�'��m�>+����0;W�(�F��|�����w�S�1��[�4�L4>�ttb��2����%�2�^�P���dd%hW&|b�Q��]�!ZX'��Ot+5IZ�F3�	`e�I�A^b��2Q�~��\��ooW)\�Op�@:y�F���|��'U��jrH>�kq�>H�2�0t2	�S���������X�FL��b�q�9!�t&�!N?������Z�j�p��s�c���3ySb$|�}���/
�H����[���.<|��RK~�Rs�����K|_�c���m�R�6\�9�/���&�JN�{��`G�T����������I1�DW�n8�wA|C����#�F�H�M�C�����NaW��:��5��M����~��7�f<
#�8���������!z��K�~q���������������/��" �2��/�<�
W���f���&Y�����\���~	�+b)k��0���'�?�����M��Es�
�j�R�/��)�?��E��D�$qT��:uL�<�y�)�����u����U���3].|�s���6	���5�N����d�4ug�q��wv����<~c����R��������f�B|*o�\	��+����Dm��9�L��L��C� �L���-�1)|1�i�B����%�m��%�\��
����!���.��
��#?B�~����`J�&�p��{��*B
l6I�.��
X��R����,�#%�?����@����3��2��j�Q���|��e��06���{H�(���{���w-iW:Z�!�'Y���N��*"#��,��
[��P�]X��%[��`.`q����`z{��`�z<���l����#�c(���������E#����,���
00�s�E�q���H7�����w�a���u��~�.��������K�����A)�WP�9N����a�_�`?]&|@����`.�]�#�,�����v����fDZ#�����9��@�OaW��9l��o��V$*���|��_&���"������'W���A��]pOw�I]�y�e��$�&H�)�&r�2���+�1�
�)�����bA���G m�&}��pF���s����*����4�;��`r����jb�ed��&g��Z^X�Z������g��mH�I
y���S���K��f_=e�O�D���,L>�X��m{G;�K�Jz���X��".��P��(��%!�f��Y�?����H���[u-pD����dl���<w=�<����eF�k�G�9�\���R���jD���I�T��$k�F���P����h�"�D���?Q�q(��������O�tv�vg�S,M�&IAj�]!�����;�m����������Q�=1k���p���E������p�zm�4){��e���A�m��r�y3�~�U6=d�8�KU�����AwX�������t���
|�j��%�P���E����)�^�O�]DKqQj<����x��;�Y�B�8U������� 
Q��blG�wr��8����N�l���0k>��IN�r���k��,���&�MV�X�UO�o����y~��/�����c=�HX��z,��M|���b��N>�o0��B�x�[�.<�5C|�';�-F����!���-���(�L������6�q�&Q��@!�!��?�,-���J��-��\<�y��[�VE�s�yBI�GQ���������1���8�'��W�|.bF�N������q?�{��7<=�RN�\7!N��Y~{�����!TIW�h�zL�I:K�n�o��@����=
S������G���yp����h���[�Fk���Zk������JK������c��X�'�����X�`�@��'Q�+���K��Cg6fi�Rm�����������z	*ob�Y�h~*�%7�
��q����'��Q����������`@H�����;�e��
��F�ws:�������NK���m��m��x��\�3������Y������4�V�Y�JmI2�w����{�b;3ql~c�p�]Vj<\�o�����J��C'��`8K �h!�����P���%�q�xG{�HiL��x�Kl�&�r�|/:�����a������3���v-��N�g %���}��Q���Ls�����;Ro����;S�I����'t��	�=��R|���@��fB�-bQ���A�U��j��i��0p��+`��;0���������s��0�.C`u��oP�$wl��/����x=���e��
��h�)�4�k{-��'��I5��Xp0��1��������y�v�Q$i�b�����q���d��7%n��'@��5���S.���juZng`�g��|2��"���&+Jn��m�.8��/�����2��'��7E��J?�l�|$��L�|�G��4��n���1MG�������k�p��p��$N&8���i����V�m��u6�'�^������iK<2�5g�P�L��l�&��X:I������v�-��p7o|o)4\;���ZFC���b��������Z����Er"�Q�$�����
�[�z�?2�(nm���l��n�k)[�O
46����A'��gT�5�W��?��mfA�)aD�xB�I��.��g�!Mid��V���a���pu�n��8ou}��xg�fw&���"\��"���.zKse�.���Fo��A��]�i�<�������^���9����*J��v����L��_C���R�:d3��2`�R�K��:�CR��i H�9,[vQ2\�#��=��bc�7���R��M
#��
Ey��+���13�N�N���1Q�%
�L�,��r�k(A��X��5��$�E��<����8s~�C�a�%����l����qL�
����� *)�-��4��]�?���>���e�$OjN��_)V��u���������}��l����R�3���^��W��nCS��iV��y_���O�-6��^�����������>.On7���������y�-����i�D�Y�d�	se��i�k��m�|����
�������7 ��r���W;�%6u�!�/��\���;o�'�'����#@P�X���Z���Gb���y��(�_�6e�����Ek�crl���PP^V�/6���Xavo	G�����s����y#�D���?D������9�}�}��":���e�����9my��
�����CL[�"�>�F�~��aS_r�4���i��������3(X� ��^�����qT��D�M������I�$�CA�C!��������@B�!,m}lt:�����:�|�c�>��������(a�N L2
�3�y�"x���>�D�_>#�85�=�yh9��W�f��7�C����%��`(���>cZA�P�Y-/D���-�F�8S[	���
2�O����D1��#0�����\�0����`�+�CU���I�#0�U�kS���=��c��=Q8�|�D�����2�a�h~����u������e�2�H���KUA	�E�Z?	��%���i���/�vj� �L3�V�o�g�a��3����v;�X�%��r~>	��+6D��Q����f2*���?��������p��,��f�%�f��`�=�ZT���l���k��]|b�>��v]�>��F�����Fo��/
6G����wT�W�o�9�����������e�������w1��xd�(f���7�z�y������8\y���y8}_��zi���%<��+����|}I�~��������]�(�uL��"\�����6~���b��&6������
����9��P.���~aw1���2���g��f��mc-�"#)������n����g����D������_���
�< c����M����B`���$��b�h$A�����r�C���Zg���l�d�enFI)"��?��QTt�'��F��$�/[����`�����B ��7����`1�U 3�����	��	��I&���Q��f3�`���V����;�@J�����L��V#1��"AK>
�8���������<w�g�e��y�l���Q���-��������3��6������_��So@���`�+@�f��Y�2��A�b����p�����8a)�0E$��>(�'|�}S�5I�C���&���H;��	$k���b$+;�����h�7?*�V������[J����l���JjR��w���E�0&a���~����lY�7>�����R�bH#�-<�M�}%���]�+��}���f����������q�c���-��}��+��2���p0�V�	c��!x��9�7������-��	b�e����*\��,]�RES�����N���(XVA��3c\��|��i�-�k-��SuK�����F�rrc�������8��6r������7R��}q��T�TK&����������)�2J�~1Q��Ze���P�O|X������Q��[��V���J~��r�1��6���,�D�������t��5�}���1p����V�,��I�%�ac��
��jA���Z}
j4�������,�����'�������,��7�k�btd|Out���s�����t��y��y0mw�V����n=	S�V�<.���I�$H�J�a���/<�WX�s��t��,�!��]�������N�E�e��
�Nd�tm���kh`�L�P�%gU\�k]9R����d�]����Di����9w^���������8�����������LB�����F%,dB�h��R��\\���������;:�P	R]����������K#�<�$m�D�\c�$��`�������Y�.D�p�������*�V9��Z��woO�/'�g����D=$��Z�=�m�ZmkH0�h��;�:4��T+��2N�����9��o�'�B,���L����p���jH(sT�c�rg�q�����Ypo!Y�PR�H�)@��|�f�J�����#~�����8�A�,�,�-�����������s������@O5�_��|��,�)�"����Ixe�n4����v�.I>�wN��m��o�u�}���x�������:���xk8�g��������s<<y�n�f��=s�#V�n����_�1V��Z��QQ�A(�@���$��%#��o����5�pG�Yx�R��z��Yr�i!0�M[R.��=�$ ���'U��XOX����l������B��;������g�SGR��D�	�O#~��F|)sTd/���n������/��*��Z��DJ_���"'��wVR�3��;�����;�?`�qA���j\�_X��\PG�n����H���������5�!*B���I����)	���������
#zejHU�L$��<V/���t���$k��U�����:}3�~��������4��~4�w�}>��F����d�v$������dg�A���	������C���	��l������DC�kX+dS,#�H4 �M-�Z)7�-no4��|�H�:/�,Ce��EFV�����+�z�&/�oK8_�LS_D��:��������H���%|.�X�P<hxyS_1�"������i�I�HF�.�$��I�]�L���6,����%QI~����Ys����z��k�Y�����s��E�_�?/��`�
pASS�8�kJ������,O�]����KX�{�I�K=�� �1�LK~�����W��]�\W,��V�j��\�\���������"rC�&<���
��>{8��1�Im��W�X�?J���?U@�����i�t�=0��xax�Nn��3�[&o�Ll�����z;�o'��U3���T���#��H�-g��?dCi���\<�X���������L������2X�+�\�K��,
x���`�O���@�f��?��/�F_9�8�������f��a��o���i��5ao����i��|�*k@/�k�����n1�{�~���6�*
��0��v���@%~]����0�I0���]b��*�D����n�m�������R��3������<:9�}���]z	��������b�{F����-��v1D���=2_r��������\���S�2�
�=�>,Y{�2�����#���������da2ZT���p4��**�kc�d[M4�C,��f3A�bG$����jt�R�Jb��O6A�/S�����v�]z+v��������`���"\�{0.s�=)}drG,[5.��<o�$��4��	���{��gc�'_w����;�e'�Dl�	�������_����`^�;9@�C��+�������zQ�����I��<�%�Ne�Um�S�-��[?3�F��|���
	� ��	����d����r��&A�	F��#	i|�����#Z����������z>���u���5��E����������}p;����
�R~�r�Lq��&��OF{��a������b�RU��TB�b����V��R#	@6��$����%�X����k�v5���r�@EH��t���7��o�L+BA�,��/��U����v�Ei�3
��<�W�����9�9{����!�BC<~����Ta����	L����M`uxTn���7�#� �����UyJ���s�~�5�qj���!���V��g�����VE�


T�����Wh��`vl��Q1�W+o1������q[��@��X��i:��G�+rx$����s�d:�2Bo������A�����J
R�D�]p��JZ":W����*KX�R��2Da�MK@���V<�A�/���(��u�%2F�=_�w�]�9�[�9���?+Eq����?��d�������.\�W�o�?"�?�zjZ�:�,CX�|2EGE���(e����s���?�����/��,$��K��
Ee��p�vxT�2/���y�Q�\b��
gG�lp#�������*��Z#w���&�AC�P�d���!i��
i��O�HG����C&7��0HL�c����������z9#Y�T�0+V����bzana�K��yV�#��k����j��J�2Lo8�5�j|
���LE`l�U	V�L#�VI���b^���m�����\LPfI���_��M�hnT��a\��P�Yoks����p����]�K��Ji*|������
��w�]t8���Juc��ER5�5Dc���X���[�j��hC�P|��=[}%B����n��xN�KU0Su]�<�bT�������H�)��*5��C*�SF���S��&m%m�N!��d\�7o0(#&E
�9����>.����+����Mf��:5b���R���J��m��xd�Y��{PUEj�}�N��O��M^E^�6��A����������#�I���dDcaU��!�u|����vb^��q�v�z1���`��g��Z�T�"�LV1���(�z v�n�W/��~��I|9O���c��p�5x�����R���3�������1gF��o�x��L��x��wXL?����Z`����0r�roAC�����B�_�c/��h��8J��{��l���Y(Y��������+N&�����PKz���l�o*���&���4�J��KV
� NvN��d��Uw��
���E��
�1��)tk4:��}$��g,��g���X���s���-�O�"��^d����9>�vx�d&���Z�g��g����YY����������e����rnf�X�`���lH���X)��g��:U6�yV����G�%�e�������_?]U�q�W�����u���J�f��R}H��T�����O�����?�[��p2V��Er&���Jj��;����/��g�������0iLE��|�;{�(Z�
��PO8�q�g_2a�s�sT�A�/���	����L<�I����4�R"	4-�K���Jxa��w
#126Andres Freund
andres@anarazel.de
In reply to: Andres Freund (#19)
Re: Command Triggers

On Sunday, December 04, 2011 02:09:08 AM Andres Freund wrote:

First, does anybody think it would be worth getting rid of the duplication
from OpenIntoRel (formerly from execMain.c) in regard to DefineRelation()?
I noticed that there already is some diversion between both. E.g. CREATE
TABLE frak TABLESPACE pg_global AS SELECT 1; is possible while it would
be forbidden via a plain CREATE TABLE. (I will send a fix for this
separately).

Sorry for letting this slide.

Is it worth adding this bit to OpenIntoRel? Not sure if there is danger in
allowing anyone to create shared tables

/* In all cases disallow placing user relations in pg_global */
if (tablespaceId == GLOBALTABLESPACE_OID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("only shared relations can be placed in pg_global
tablespace")));

Andres

#127Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#126)
Re: Command Triggers

Andres Freund <andres@anarazel.de> writes:

Sorry for letting this slide.

Is it worth adding this bit to OpenIntoRel? Not sure if there is danger in
allowing anyone to create shared tables

/* In all cases disallow placing user relations in pg_global */
if (tablespaceId == GLOBALTABLESPACE_OID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("only shared relations can be placed in pg_global
tablespace")));

Ugh ... if that's currently allowed, we definitely need to fix it.
But I'm not sure OpenIntoRel is the right place. I'd have expected
the test to be at some lower level, like heap_create_with_catalog
or so.

regards, tom lane

#128Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#127)
Re: Command Triggers

On Tuesday, February 28, 2012 12:30:36 AM Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

Sorry for letting this slide.

Is it worth adding this bit to OpenIntoRel? Not sure if there is danger
in allowing anyone to create shared tables

/* In all cases disallow placing user relations in pg_global */
if (tablespaceId == GLOBALTABLESPACE_OID)

ereport(ERROR,

(errcode(ERRCODE_INVALID_PARAMETER_VALUE),

errmsg("only shared relations can be placed in pg_global

tablespace")));

Ugh ... if that's currently allowed, we definitely need to fix it.
But I'm not sure OpenIntoRel is the right place. I'd have expected
the test to be at some lower level, like heap_create_with_catalog
or so.

Its definitely allowed right now:

test-upgrade=# CREATE TABLE foo TABLESPACE pg_global AS SELECT 1;
SELECT 1
Time: 354.097 ms

The analogous check for the missing one in OpenIntoRel for plain relations is
in defineRelation. heap_create_with_catalog only contains the inverse check:

/*
* Shared relations must be in pg_global (last-ditch check)
*/
if (shared_relation && reltablespace != GLOBALTABLESPACE_OID)
elog(ERROR, "shared relations must be placed in pg_global
tablespace");

Moving it there sounds like a good idea without any problem I can see right
now. Want me to prepare a patch or is it just the same for you if you do it
yourself?

Andres

#129Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#127)
Re: Command Triggers

On Tuesday, February 28, 2012 12:30:36 AM Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

Sorry for letting this slide.

Is it worth adding this bit to OpenIntoRel? Not sure if there is danger
in allowing anyone to create shared tables

/* In all cases disallow placing user relations in pg_global */
if (tablespaceId == GLOBALTABLESPACE_OID)

ereport(ERROR,

(errcode(ERRCODE_INVALID_PARAMETER_VALUE),

errmsg("only shared relations can be placed in pg_global

tablespace")));

Ugh ... if that's currently allowed, we definitely need to fix it.

Btw, whats the danger youre seing?

Andres

#130Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#129)
Re: Command Triggers

Andres Freund <andres@anarazel.de> writes:

On Tuesday, February 28, 2012 12:30:36 AM Tom Lane wrote:

Ugh ... if that's currently allowed, we definitely need to fix it.

Btw, whats the danger youre seing?

Well, I'm not sure that it would actively break anything, but we
definitely meant to disallow the case. Also, I seem to recall some
places that intuit a relation's shared marker in the opposite direction
(if it's in pg_global it must be shared), and that could definitely
cause issues if we treat a rel as shared when it isn't.

regards, tom lane

#131Alvaro Herrera
alvherre@commandprompt.com
In reply to: Tom Lane (#130)
Re: Command Triggers

Excerpts from Tom Lane's message of lun feb 27 20:54:41 -0300 2012:

Andres Freund <andres@anarazel.de> writes:

On Tuesday, February 28, 2012 12:30:36 AM Tom Lane wrote:

Ugh ... if that's currently allowed, we definitely need to fix it.

Btw, whats the danger youre seing?

Well, I'm not sure that it would actively break anything, but we
definitely meant to disallow the case. Also, I seem to recall some
places that intuit a relation's shared marker in the opposite direction
(if it's in pg_global it must be shared), and that could definitely
cause issues if we treat a rel as shared when it isn't.

The list of shared rels is hardcoded -- see IsSharedRelation.

--
Álvaro Herrera <alvherre@commandprompt.com>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support

#132Andres Freund
andres@anarazel.de
In reply to: Andres Freund (#128)
1 attachment(s)
Re: Command Triggers

On Tuesday, February 28, 2012 12:43:02 AM Andres Freund wrote:

On Tuesday, February 28, 2012 12:30:36 AM Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

Sorry for letting this slide.

Is it worth adding this bit to OpenIntoRel? Not sure if there is danger
in allowing anyone to create shared tables

/* In all cases disallow placing user relations in pg_global */
if (tablespaceId == GLOBALTABLESPACE_OID)

ereport(ERROR,

(errcode(ERRCODE_INVALID_PARAMETER_VALUE),

errmsg("only shared relations can be placed in pg_global

tablespace")));

Ugh ... if that's currently allowed, we definitely need to fix it.
But I'm not sure OpenIntoRel is the right place. I'd have expected
the test to be at some lower level, like heap_create_with_catalog
or so.

Its definitely allowed right now:

test-upgrade=# CREATE TABLE foo TABLESPACE pg_global AS SELECT 1;
SELECT 1
Time: 354.097 ms

The analogous check for the missing one in OpenIntoRel for plain relations
is in defineRelation. heap_create_with_catalog only contains the inverse
check:

/*
* Shared relations must be in pg_global (last-ditch check)
*/
if (shared_relation && reltablespace != GLOBALTABLESPACE_OID)
elog(ERROR, "shared relations must be placed in pg_global
tablespace");

Moving it there sounds like a good idea without any problem I can see right
now. Want me to prepare a patch or is it just the same for you if you do it
yourself?

Sorry to bother you with that dreary topic further, but this should probably
be fixed before the next set of stable releases.

The check cannot easily be moved to heap_create_with_catalog because e.g.
cluster.c's make_new_heap does heap_create_with_catalog for a temporary copy
of shared relations without being able to mark them as such (because they
don't have the right oid and thus IsSharedRelation would cry). So I think just
adding the same check to the ctas code as the normal DefineRelation contains
is the best way forward.

The attached patch applies from 8.3 to 9.1 (8.2 has conflicts but
thankfully...).

Andres

Attachments:

0001-Check-that-the-specified-tablespace-in-a-CREATE-TABL.patchtext/x-patch; charset=UTF-8; name=0001-Check-that-the-specified-tablespace-in-a-CREATE-TABL.patchDownload
From 77896a7385d1ef5c793e06c5085a8b37ab5857c9 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 20 Mar 2012 16:33:13 +0100
Subject: [PATCH] Check that the specified tablespace in a CREATE TABLE AS
 command is not pg_global

That check was not added to the CTAS code when it was added to the ordinary
CREATE TABLE AS.

It might be nicer to add that check heap_create_with_catalog as well, but thats
not easily possible because e.g. cluster creates a temporary new heap which
cannot be marked shared because there is a fixed list of shared relations (see
IsSharedRelation).
---
 src/backend/executor/execMain.c |    7 +++++++
 1 files changed, 7 insertions(+), 0 deletions(-)

diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 621ad8a..8a43db7 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -43,6 +43,7 @@
 #include "access/xact.h"
 #include "catalog/heap.h"
 #include "catalog/namespace.h"
+#include "catalog/pg_tablespace.h"
 #include "catalog/toasting.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
@@ -2452,6 +2453,12 @@ OpenIntoRel(QueryDesc *queryDesc)
 						   get_tablespace_name(tablespaceId));
 	}
 
+	/* In all cases disallow placing user relations in pg_global */
+	if (tablespaceId == GLOBALTABLESPACE_OID)
+		ereport(ERROR,
+		        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		         errmsg("only shared relations can be placed in pg_global tablespace")));
+
 	/* Parse and validate any reloptions */
 	reloptions = transformRelOptions((Datum) 0,
 									 into->options,
-- 
1.7.6.409.ge7a85.dirty

#133Robert Haas
robertmhaas@gmail.com
In reply to: Andres Freund (#132)
Re: Command Triggers

On Tue, Mar 20, 2012 at 12:02 PM, Andres Freund <andres@anarazel.de> wrote:

The attached patch applies from 8.3 to 9.1 (8.2 has conflicts but
thankfully...).

It seems it doesn't apply to master (any more?).

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

#134Andres Freund
andres@anarazel.de
In reply to: Robert Haas (#133)
Re: Command Triggers

On Wednesday, March 21, 2012 04:54:00 PM Robert Haas wrote:

On Tue, Mar 20, 2012 at 12:02 PM, Andres Freund <andres@anarazel.de> wrote:

The attached patch applies from 8.3 to 9.1 (8.2 has conflicts but
thankfully...).

It seems it doesn't apply to master (any more?).

Its not required there because of the unification of CTAS with normal code.
Sorry, that was only clear from a few mails away, should have made that
explicit.

Andres

#135Robert Haas
robertmhaas@gmail.com
In reply to: Andres Freund (#134)
Re: Command Triggers

On Wed, Mar 21, 2012 at 11:58 AM, Andres Freund <andres@anarazel.de> wrote:

On Wednesday, March 21, 2012 04:54:00 PM Robert Haas wrote:

On Tue, Mar 20, 2012 at 12:02 PM, Andres Freund <andres@anarazel.de> wrote:

The attached patch applies from 8.3 to 9.1 (8.2 has conflicts but
thankfully...).

It seems it doesn't apply to master (any more?).

Its not required there because of the unification of CTAS with normal code.
Sorry, that was only clear from a few mails away, should have made that
explicit.

Or maybe I should have tested it before jumping to conclusions.

Anyway, I've committed the patch to 9.1, 9.0, 8.4, and 8.3.

Thanks,

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