SQL procedures

Started by Peter Eisentrautabout 8 years ago70 messages
#1Peter Eisentraut
peter.eisentraut@2ndquadrant.com
1 attachment(s)

I've been working on SQL procedures. (Some might call them "stored
procedures", but I'm not aware of any procedures that are not stored, so
that's not a term that I'm using here.)

Everything that follows is intended to align with the SQL standard, at
least in spirit.

This first patch does a bunch of preparation work. It adds the
CREATE/ALTER/DROP PROCEDURE commands and the CALL statement to call a
procedure. It also adds ROUTINE syntax which can refer to a function or
procedure. I have extended that to include aggregates. And then there
is a bunch of leg work, such as psql and pg_dump support. The
documentation is a lot of copy-and-paste right now; that can be
revisited sometime. The provided procedural languages (an ever more
confusing term) each needed a small touch-up to handle pg_proc entries
with prorettype == 0.

Right now, there is no support for returning values from procedures via
OUT parameters. That will need some definitional pondering; and see
also below for a possible alternative.

With this, you can write procedures that are somewhat compatible with
DB2, MySQL, and to a lesser extent Oracle.

Separately, I will send patches that implement (the beginnings of) two
separate features on top of this:

- Transaction control in procedure bodies

- Returning multiple result sets

(In various previous discussions on "real stored procedures" or
something like that, most people seemed to have one of these two
features in mind. I think that depends on what other SQL systems one
has worked with previously.)

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

v1-0001-SQL-procedures.patchtext/plain; charset=UTF-8; name=v1-0001-SQL-procedures.patch; x-mac-creator=0; x-mac-type=0Download
From 6ea86532a25c90c8aafd5375fb9ea3da53d2dd39 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Tue, 31 Oct 2017 12:48:39 -0400
Subject: [PATCH v1] SQL procedures

CREATE/ALTER/DROP PROCEDURE, ALTER/DROP ROUTINE, CALL statement, utility
statement support, support in the built-in PLs, support in pg_dump and
psql
---
 doc/src/sgml/catalogs.sgml                     |   2 +-
 doc/src/sgml/ecpg.sgml                         |   4 +-
 doc/src/sgml/information_schema.sgml           |  18 +-
 doc/src/sgml/plpgsql.sgml                      |   2 +-
 doc/src/sgml/ref/allfiles.sgml                 |   6 +
 doc/src/sgml/ref/alter_extension.sgml          |  12 +-
 doc/src/sgml/ref/alter_procedure.sgml          | 302 ++++++++++++++++++++
 doc/src/sgml/ref/alter_routine.sgml            |  98 +++++++
 doc/src/sgml/ref/call.sgml                     |  97 +++++++
 doc/src/sgml/ref/comment.sgml                  |  13 +-
 doc/src/sgml/ref/create_function.sgml          |   4 +-
 doc/src/sgml/ref/create_procedure.sgml         | 374 +++++++++++++++++++++++++
 doc/src/sgml/ref/drop_procedure.sgml           | 161 +++++++++++
 doc/src/sgml/ref/drop_routine.sgml             |  90 ++++++
 doc/src/sgml/ref/grant.sgml                    |   4 +-
 doc/src/sgml/ref/revoke.sgml                   |   4 +-
 doc/src/sgml/ref/security_label.sgml           |  12 +-
 doc/src/sgml/reference.sgml                    |   6 +
 src/backend/catalog/aclchk.c                   |  60 +++-
 src/backend/catalog/information_schema.sql     |  25 +-
 src/backend/catalog/objectaddress.c            |  19 +-
 src/backend/catalog/pg_proc.c                  |   3 +-
 src/backend/commands/aggregatecmds.c           |   2 +-
 src/backend/commands/alter.c                   |   6 +
 src/backend/commands/dropcmds.c                |  38 ++-
 src/backend/commands/event_trigger.c           |  14 +
 src/backend/commands/functioncmds.c            | 131 ++++++++-
 src/backend/commands/opclasscmds.c             |   4 +-
 src/backend/executor/functions.c               |  13 +-
 src/backend/nodes/copyfuncs.c                  |  15 +
 src/backend/nodes/equalfuncs.c                 |  13 +
 src/backend/optimizer/util/clauses.c           |   1 +
 src/backend/parser/gram.y                      | 254 ++++++++++++++++-
 src/backend/parser/parse_agg.c                 |  11 +
 src/backend/parser/parse_expr.c                |   8 +
 src/backend/parser/parse_func.c                | 201 ++++++++-----
 src/backend/tcop/utility.c                     |  44 ++-
 src/backend/utils/adt/ruleutils.c              |   6 +
 src/backend/utils/cache/lsyscache.c            |  19 ++
 src/bin/pg_dump/dumputils.c                    |   5 +-
 src/bin/pg_dump/pg_backup_archiver.c           |   7 +-
 src/bin/pg_dump/pg_dump.c                      |  32 ++-
 src/bin/pg_dump/t/002_pg_dump.pl               |  38 +++
 src/bin/psql/describe.c                        |   8 +-
 src/bin/psql/tab-complete.c                    |  77 ++++-
 src/include/commands/defrem.h                  |   3 +-
 src/include/nodes/nodes.h                      |   1 +
 src/include/nodes/parsenodes.h                 |  17 ++
 src/include/parser/kwlist.h                    |   4 +
 src/include/parser/parse_func.h                |   8 +-
 src/include/parser/parse_node.h                |   3 +-
 src/include/utils/lsyscache.h                  |   1 +
 src/interfaces/ecpg/preproc/ecpg.tokens        |   2 +-
 src/interfaces/ecpg/preproc/ecpg.trailer       |   5 +-
 src/interfaces/ecpg/preproc/ecpg_keywords.c    |   1 -
 src/pl/plperl/GNUmakefile                      |   2 +-
 src/pl/plperl/expected/plperl_call.out         |  29 ++
 src/pl/plperl/plperl.c                         |   8 +-
 src/pl/plperl/sql/plperl_call.sql              |  36 +++
 src/pl/plpgsql/src/pl_comp.c                   |  88 +++---
 src/pl/plpgsql/src/pl_exec.c                   |   8 +-
 src/pl/plpython/Makefile                       |   1 +
 src/pl/plpython/expected/plpython_call.out     |  35 +++
 src/pl/plpython/plpy_exec.c                    |  14 +-
 src/pl/plpython/plpy_procedure.c               |   5 +-
 src/pl/plpython/plpy_procedure.h               |   3 +-
 src/pl/plpython/sql/plpython_call.sql          |  41 +++
 src/pl/tcl/Makefile                            |   2 +-
 src/pl/tcl/expected/pltcl_call.out             |  29 ++
 src/pl/tcl/pltcl.c                             |  13 +-
 src/pl/tcl/sql/pltcl_call.sql                  |  36 +++
 src/test/regress/expected/create_procedure.out |  86 ++++++
 src/test/regress/expected/object_address.out   |  15 +-
 src/test/regress/expected/plpgsql.out          |  41 +++
 src/test/regress/expected/polymorphism.out     |  16 +-
 src/test/regress/parallel_schedule             |   2 +-
 src/test/regress/serial_schedule               |   1 +
 src/test/regress/sql/create_procedure.sql      |  79 ++++++
 src/test/regress/sql/object_address.sql        |   4 +-
 src/test/regress/sql/plpgsql.sql               |  49 ++++
 80 files changed, 2679 insertions(+), 272 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_procedure.sgml
 create mode 100644 doc/src/sgml/ref/alter_routine.sgml
 create mode 100644 doc/src/sgml/ref/call.sgml
 create mode 100644 doc/src/sgml/ref/create_procedure.sgml
 create mode 100644 doc/src/sgml/ref/drop_procedure.sgml
 create mode 100644 doc/src/sgml/ref/drop_routine.sgml
 create mode 100644 src/pl/plperl/expected/plperl_call.out
 create mode 100644 src/pl/plperl/sql/plperl_call.sql
 create mode 100644 src/pl/plpython/expected/plpython_call.out
 create mode 100644 src/pl/plpython/sql/plpython_call.sql
 create mode 100644 src/pl/tcl/expected/pltcl_call.out
 create mode 100644 src/pl/tcl/sql/pltcl_call.sql
 create mode 100644 src/test/regress/expected/create_procedure.out
 create mode 100644 src/test/regress/sql/create_procedure.sql

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ef60a58631..f36930c721 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5241,7 +5241,7 @@ <title><structname>pg_proc</structname> Columns</title>
       <entry><structfield>prorettype</structfield></entry>
       <entry><type>oid</type></entry>
       <entry><literal><link linkend="catalog-pg-type"><structname>pg_type</structname></link>.oid</literal></entry>
-      <entry>Data type of the return value</entry>
+      <entry>Data type of the return value, or null for a procedure</entry>
      </row>
 
      <row>
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index bc3d080774..f503f2d35f 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4778,7 +4778,9 @@ <title>Setting Callbacks</title>
       <term><literal>DO <replaceable>name</replaceable> (<replaceable>args</replaceable>)</literal></term>
       <listitem>
        <para>
-        Call the specified C functions with the specified arguments.
+        Call the specified C functions with the specified arguments.  (This
+        use is different from the meaning of <literal>CALL</literal>
+        and <literal>DO</literal> in the normal PostgreSQL grammar.)
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 58c54254d7..ab28acc854 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -3972,8 +3972,8 @@ <title><literal>routine_privileges</literal> Columns</title>
   <title><literal>routines</literal></title>
 
   <para>
-   The view <literal>routines</literal> contains all functions in the
-   current database.  Only those functions are shown that the current
+   The view <literal>routines</literal> contains all functions and procedures in the
+   current database.  Only those functions and procedures are shown that the current
    user has access to (by way of being the owner or having some
    privilege).
   </para>
@@ -4037,8 +4037,8 @@ <title><literal>routines</literal> Columns</title>
       <entry><literal>routine_type</literal></entry>
       <entry><type>character_data</type></entry>
       <entry>
-       Always <literal>FUNCTION</literal> (In the future there might
-       be other types of routines.)
+       <literal>FUNCTION</literal> for a
+       function, <literal>PROCEDURE</literal> for a procedure
       </entry>
      </row>
 
@@ -4087,7 +4087,7 @@ <title><literal>routines</literal> Columns</title>
        the view <literal>element_types</literal>), else
        <literal>USER-DEFINED</literal> (in that case, the type is
        identified in <literal>type_udt_name</literal> and associated
-       columns).
+       columns).  Null for a procedure.
       </entry>
      </row>
 
@@ -4180,7 +4180,7 @@ <title><literal>routines</literal> Columns</title>
       <entry><type>sql_identifier</type></entry>
       <entry>
        Name of the database that the return data type of the function
-       is defined in (always the current database)
+       is defined in (always the current database).  Null for a procedure.
       </entry>
      </row>
 
@@ -4189,7 +4189,7 @@ <title><literal>routines</literal> Columns</title>
       <entry><type>sql_identifier</type></entry>
       <entry>
        Name of the schema that the return data type of the function is
-       defined in
+       defined in.  Null for a procedure.
       </entry>
      </row>
 
@@ -4197,7 +4197,7 @@ <title><literal>routines</literal> Columns</title>
       <entry><literal>type_udt_name</literal></entry>
       <entry><type>sql_identifier</type></entry>
       <entry>
-       Name of the return data type of the function
+       Name of the return data type of the function.  Null for a procedure.
       </entry>
      </row>
 
@@ -4314,7 +4314,7 @@ <title><literal>routines</literal> Columns</title>
       <entry>
        If the function automatically returns null if any of its
        arguments are null, then <literal>YES</literal>, else
-       <literal>NO</literal>.
+       <literal>NO</literal>.  Null for a procedure.
       </entry>
      </row>
 
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 7323c2f67d..b3d72b6f95 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -5244,7 +5244,7 @@ <title>Porting a Function that Creates Another Function from <application>PL/SQL
     <para>
      Here is how this function would end up in <productname>PostgreSQL</productname>:
 <programlisting>
-CREATE OR REPLACE FUNCTION cs_update_referrer_type_proc() RETURNS void AS $func$
+CREATE OR REPLACE PROCEDURE cs_update_referrer_type_proc() AS $func$
 DECLARE
     referrer_keys CURSOR IS
         SELECT * FROM cs_referrer_keys
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 01acc2ef9d..22e6893211 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -26,8 +26,10 @@
 <!ENTITY alterOperatorClass SYSTEM "alter_opclass.sgml">
 <!ENTITY alterOperatorFamily SYSTEM "alter_opfamily.sgml">
 <!ENTITY alterPolicy        SYSTEM "alter_policy.sgml">
+<!ENTITY alterProcedure     SYSTEM "alter_procedure.sgml">
 <!ENTITY alterPublication   SYSTEM "alter_publication.sgml">
 <!ENTITY alterRole          SYSTEM "alter_role.sgml">
+<!ENTITY alterRoutine       SYSTEM "alter_routine.sgml">
 <!ENTITY alterRule          SYSTEM "alter_rule.sgml">
 <!ENTITY alterSchema        SYSTEM "alter_schema.sgml">
 <!ENTITY alterServer        SYSTEM "alter_server.sgml">
@@ -48,6 +50,7 @@
 <!ENTITY alterView          SYSTEM "alter_view.sgml">
 <!ENTITY analyze            SYSTEM "analyze.sgml">
 <!ENTITY begin              SYSTEM "begin.sgml">
+<!ENTITY call               SYSTEM "call.sgml">
 <!ENTITY checkpoint         SYSTEM "checkpoint.sgml">
 <!ENTITY close              SYSTEM "close.sgml">
 <!ENTITY cluster            SYSTEM "cluster.sgml">
@@ -75,6 +78,7 @@
 <!ENTITY createOperatorClass SYSTEM "create_opclass.sgml">
 <!ENTITY createOperatorFamily SYSTEM "create_opfamily.sgml">
 <!ENTITY createPolicy       SYSTEM "create_policy.sgml">
+<!ENTITY createProcedure    SYSTEM "create_procedure.sgml">
 <!ENTITY createPublication  SYSTEM "create_publication.sgml">
 <!ENTITY createRole         SYSTEM "create_role.sgml">
 <!ENTITY createRule         SYSTEM "create_rule.sgml">
@@ -122,8 +126,10 @@
 <!ENTITY dropOperatorFamily  SYSTEM "drop_opfamily.sgml">
 <!ENTITY dropOwned          SYSTEM "drop_owned.sgml">
 <!ENTITY dropPolicy         SYSTEM "drop_policy.sgml">
+<!ENTITY dropProcedure      SYSTEM "drop_procedure.sgml">
 <!ENTITY dropPublication    SYSTEM "drop_publication.sgml">
 <!ENTITY dropRole           SYSTEM "drop_role.sgml">
+<!ENTITY dropRoutine        SYSTEM "drop_routine.sgml">
 <!ENTITY dropRule           SYSTEM "drop_rule.sgml">
 <!ENTITY dropSchema         SYSTEM "drop_schema.sgml">
 <!ENTITY dropSequence       SYSTEM "drop_sequence.sgml">
diff --git a/doc/src/sgml/ref/alter_extension.sgml b/doc/src/sgml/ref/alter_extension.sgml
index c2b0669c38..d83b0f1dbb 100644
--- a/doc/src/sgml/ref/alter_extension.sgml
+++ b/doc/src/sgml/ref/alter_extension.sgml
@@ -45,6 +45,8 @@
   OPERATOR CLASS <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   OPERATOR FAMILY <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
+  PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
+  ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   SCHEMA <replaceable class="parameter">object_name</replaceable> |
   SEQUENCE <replaceable class="parameter">object_name</replaceable> |
   SERVER <replaceable class="parameter">object_name</replaceable> |
@@ -170,12 +172,14 @@ <title>Parameters</title>
      <term><replaceable class="parameter">aggregate_name</replaceable></term>
      <term><replaceable class="parameter">function_name</replaceable></term>
      <term><replaceable class="parameter">operator_name</replaceable></term>
+     <term><replaceable class="parameter">procedure_name</replaceable></term>
+     <term><replaceable class="parameter">routine_name</replaceable></term>
      <listitem>
       <para>
        The name of an object to be added to or removed from the extension.
        Names of tables,
        aggregates, domains, foreign tables, functions, operators,
-       operator classes, operator families, sequences, text search objects,
+       operator classes, operator families, procedures, routines, sequences, text search objects,
        types, and views can be schema-qualified.
       </para>
      </listitem>
@@ -204,7 +208,7 @@ <title>Parameters</title>
 
      <listitem>
       <para>
-       The mode of a function or aggregate
+       The mode of a function, procedure, or aggregate
        argument: <literal>IN</literal>, <literal>OUT</literal>,
        <literal>INOUT</literal>, or <literal>VARIADIC</literal>.
        If omitted, the default is <literal>IN</literal>.
@@ -222,7 +226,7 @@ <title>Parameters</title>
 
      <listitem>
       <para>
-       The name of a function or aggregate argument.
+       The name of a function, procedure, or aggregate argument.
        Note that <command>ALTER EXTENSION</command> does not actually pay
        any attention to argument names, since only the argument data
        types are needed to determine the function's identity.
@@ -235,7 +239,7 @@ <title>Parameters</title>
 
      <listitem>
       <para>
-       The data type of a function or aggregate argument.
+       The data type of a function, procedure, or aggregate argument.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/alter_procedure.sgml b/doc/src/sgml/ref/alter_procedure.sgml
new file mode 100644
index 0000000000..2863eefb07
--- /dev/null
+++ b/doc/src/sgml/ref/alter_procedure.sgml
@@ -0,0 +1,302 @@
+<!--
+doc/src/sgml/ref/alter_procedure.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterprocedure">
+ <indexterm zone="sql-alterprocedure">
+  <primary>ALTER PROCEDURE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER PROCEDURE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER PROCEDURE</refname>
+  <refpurpose>change the definition of a procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    <replaceable class="parameter">action</replaceable> [ ... ] [ RESTRICT ]
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    RENAME TO <replaceable>new_name</replaceable>
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_USER | SESSION_USER }
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    SET SCHEMA <replaceable>new_schema</replaceable>
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    DEPENDS ON EXTENSION <replaceable>extension_name</replaceable>
+
+<phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
+
+    IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF
+    [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
+    PARALLEL { UNSAFE | RESTRICTED | SAFE }
+    COST <replaceable class="parameter">execution_cost</replaceable>
+    ROWS <replaceable class="parameter">result_rows</replaceable>
+    SET <replaceable class="parameter">configuration_parameter</replaceable> { TO | = } { <replaceable class="parameter">value</replaceable> | DEFAULT }
+    SET <replaceable class="parameter">configuration_parameter</replaceable> FROM CURRENT
+    RESET <replaceable class="parameter">configuration_parameter</replaceable>
+    RESET ALL
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER PROCEDURE</command> changes the definition of a
+   procedure.
+  </para>
+
+  <para>
+   You must own the procedure to use <command>ALTER PROCEDURE</command>.
+   To change a procedure's schema, you must also have <literal>CREATE</literal>
+   privilege on the new schema.
+   To alter the owner, you must also be a direct or indirect member of the new
+   owning role, and that role must have <literal>CREATE</literal> privilege on
+   the procedure's schema.  (These restrictions enforce that altering the owner
+   doesn't do anything you couldn't do by dropping and recreating the procedure.
+   However, a superuser can alter ownership of any procedure anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing procedure.  If no
+      argument list is specified, the name must be unique in its schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argmode</replaceable></term>
+
+    <listitem>
+     <para>
+      The mode of an argument: <literal>IN</literal>  or <literal>VARIADIC</literal>.
+      If omitted, the default is <literal>IN</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argname</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of an argument.
+      Note that <command>ALTER PROCEDURE</command> does not actually pay
+      any attention to argument names, since only the argument data
+      types are needed to determine the procedure's identity.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argtype</replaceable></term>
+
+    <listitem>
+     <para>
+      The data type(s) of the procedure's arguments (optionally
+      schema-qualified), if any.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the procedure.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the procedure.  Note that if the procedure is
+      marked <literal>SECURITY DEFINER</literal>, it will subsequently
+      execute as the new owner.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the procedure.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">extension_name</replaceable></term>
+    <listitem>
+     <para>
+      The name of the extension that the procedure is to depend on.
+     </para>
+    </listitem>
+   </varlistentry>
+
+    <varlistentry>
+     <term><literal>IMMUTABLE</literal></term>
+     <term><literal>STABLE</literal></term>
+     <term><literal>VOLATILE</literal></term>
+     <term><literal>PARALLEL</literal></term>
+     <term><literal>LEAKPROOF</literal></term>
+     <term><literal>COST</literal> <replaceable class="parameter">execution_cost</replaceable></term>
+     <term><literal>ROWS</literal> <replaceable class="parameter">result_rows</replaceable></term>
+
+     <listitem>
+      <para>
+       These attributes are accepted for compatibility
+       with <xref linkend="sql-alterfunction"> but are currently not used for
+       procedures.
+      </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal><optional> EXTERNAL </optional> SECURITY INVOKER</literal></term>
+    <term><literal><optional> EXTERNAL </optional> SECURITY DEFINER</literal></term>
+
+    <listitem>
+     <para>
+      Change whether the procedure is a security definer or not. The
+      key word <literal>EXTERNAL</literal> is ignored for SQL
+      conformance. See <xref linkend="sql-createprocedure"> for more information about
+      this capability.
+     </para>
+    </listitem>
+   </varlistentry>
+
+     <varlistentry>
+      <term><replaceable>configuration_parameter</replaceable></term>
+      <term><replaceable>value</replaceable></term>
+      <listitem>
+       <para>
+        Add or change the assignment to be made to a configuration parameter
+        when the procedure is called.  If
+        <replaceable>value</replaceable> is <literal>DEFAULT</literal>
+        or, equivalently, <literal>RESET</literal> is used, the procedure-local
+        setting is removed, so that the procedure executes with the value
+        present in its environment.  Use <literal>RESET
+        ALL</literal> to clear all procedure-local settings.
+        <literal>SET FROM CURRENT</literal> saves the value of the parameter that
+        is current when <command>ALTER PROCEDURE</command> is executed as the value
+        to be applied when the procedure is entered.
+       </para>
+
+       <para>
+        See <xref linkend="sql-set"> and
+        <xref linkend="runtime-config">
+        for more information about allowed parameter names and values.
+       </para>
+      </listitem>
+     </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+
+    <listitem>
+     <para>
+      Ignored for conformance with the SQL standard.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the procedure <literal>sqrt</literal> for type
+   <type>integer</type> to <literal>square_root</literal>:
+<programlisting>
+ALTER PROCEDURE sqrt(integer) RENAME TO square_root;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the procedure <literal>sqrt</literal> for type
+   <type>integer</type> to <literal>joe</literal>:
+<programlisting>
+ALTER PROCEDURE sqrt(integer) OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To change the schema of the procedure <literal>sqrt</literal> for type
+   <type>integer</type> to <literal>maths</literal>:
+<programlisting>
+ALTER PROCEDURE sqrt(integer) SET SCHEMA maths;
+</programlisting>
+  </para>
+
+  <para>
+   To mark the procedure <literal>sqrt</literal> for type
+   <type>integer</type> as being dependent on the extension
+   <literal>mathlib</literal>:
+<programlisting>
+ALTER PROCEDURE sqrt(integer) DEPENDS ON EXTENSION mathlib;
+</programlisting>
+  </para>
+
+  <para>
+   To adjust the search path that is automatically set for a procedure:
+<programlisting>
+ALTER PROCEDURE check_password(text) SET search_path = admin, pg_temp;
+</programlisting>
+  </para>
+
+  <para>
+   To disable automatic setting of <varname>search_path</varname> for a procedure:
+<programlisting>
+ALTER PROCEDURE check_password(text) RESET search_path;
+</programlisting>
+   The procedure will now execute with whatever search path is used by its
+   caller.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   This statement is partially compatible with the <command>ALTER
+   PROCEDURE</command> statement in the SQL standard. The standard allows more
+   properties of a procedure to be modified, but does not provide the
+   ability to rename a procedure, make a procedure a security definer,
+   attach configuration parameter values to a procedure,
+   or change the owner, schema, or volatility of a procedure. The standard also
+   requires the <literal>RESTRICT</literal> key word, which is optional in
+   <productname>PostgreSQL</productname>.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createprocedure"></member>
+   <member><xref linkend="sql-dropprocedure"></member>
+   <member><xref linkend="sql-alterfunction"></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_routine.sgml b/doc/src/sgml/ref/alter_routine.sgml
new file mode 100644
index 0000000000..821fab0ddb
--- /dev/null
+++ b/doc/src/sgml/ref/alter_routine.sgml
@@ -0,0 +1,98 @@
+<!--
+doc/src/sgml/ref/alter_routine.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterroutine">
+ <indexterm zone="sql-alterroutine">
+  <primary>ALTER ROUTINE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER ROUTINE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER ROUTINE</refname>
+  <refpurpose>change the definition of a routine</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    <replaceable class="parameter">action</replaceable> [ ... ] [ RESTRICT ]
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    RENAME TO <replaceable>new_name</replaceable>
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_USER | SESSION_USER }
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    SET SCHEMA <replaceable>new_schema</replaceable>
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    DEPENDS ON EXTENSION <replaceable>extension_name</replaceable>
+
+<phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
+
+    IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF
+    [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
+    PARALLEL { UNSAFE | RESTRICTED | SAFE }
+    COST <replaceable class="parameter">execution_cost</replaceable>
+    ROWS <replaceable class="parameter">result_rows</replaceable>
+    SET <replaceable class="parameter">configuration_parameter</replaceable> { TO | = } { <replaceable class="parameter">value</replaceable> | DEFAULT }
+    SET <replaceable class="parameter">configuration_parameter</replaceable> FROM CURRENT
+    RESET <replaceable class="parameter">configuration_parameter</replaceable>
+    RESET ALL
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER ROUTINE</command> changes the definition of a routine, which
+   can be an aggregate function, a normal function, or a procedure.  See
+   under <xref linkend="sql-alteraggregate">, <xref linkend="sql-alterfunction">,
+   and <xref linkend="sql-alterprocedure"> for the description of the
+   parameters, more examples, and further details.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the routine <literal>foo</literal> for type
+   <type>integer</type> to <literal>foobar</literal>:
+<programlisting>
+ALTER ROUTINE foo(integer) RENAME TO foobar;
+</programlisting>
+   This command will work independent of whether <literal>foo</literal> is an
+   aggregate, function, or procedure.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   This statement is partially compatible with the <command>ALTER
+   ROUTINE</command> statement in the SQL standard.  See
+   under <xref linkend="sql-alterfunction">
+   and <xref linkend="sql-alterprocedure"> for more details.  Allowing
+   routine names to refer to aggregate functions is
+   a <productname>PostgreSQL</productname> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alteraggregate"></member>
+   <member><xref linkend="sql-alterfunction"></member>
+   <member><xref linkend="sql-alterprocedure"></member>
+   <member><xref linkend="sql-droproutine"></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/call.sgml b/doc/src/sgml/ref/call.sgml
new file mode 100644
index 0000000000..a0f7e6d888
--- /dev/null
+++ b/doc/src/sgml/ref/call.sgml
@@ -0,0 +1,97 @@
+<!--
+doc/src/sgml/ref/call.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-call">
+ <indexterm zone="sql-call">
+  <primary>CALL</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CALL</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CALL</refname>
+  <refpurpose>invoke a procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CALL <replaceable class="parameter">name</replaceable> ( [ <replaceable class="parameter">argument</replaceable> ] [ , ...] )
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CALL</command> executes a procedure.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the procedure.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  <varlistentry>
+    <term><replaceable class="parameter">argument</replaceable></term>
+    <listitem>
+     <para>
+      An argument for the procedure call.
+      See <xref linkend="sql-syntax-calling-funcs"> for the full details on
+      function and procedure call syntax, including use of named parameters.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The user must have <literal>EXECUTE</literal> privilege on the procedure in
+   order to be allowed to invoke it.
+  </para>
+
+  <para>
+   To call a function (not a procedure), use <command>SELECT</command> instead.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+<programlisting>
+CALL do_db_maintenance();
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CALL</command> conforms to the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createprocedure"></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index d705792a45..1c2aa2a2b5 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -46,8 +46,10 @@
   OPERATOR FAMILY <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   POLICY <replaceable class="parameter">policy_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
+  PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   PUBLICATION <replaceable class="parameter">object_name</replaceable> |
   ROLE <replaceable class="parameter">object_name</replaceable> |
+  ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   RULE <replaceable class="parameter">rule_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   SCHEMA <replaceable class="parameter">object_name</replaceable> |
   SEQUENCE <replaceable class="parameter">object_name</replaceable> |
@@ -121,13 +123,15 @@ <title>Parameters</title>
     <term><replaceable class="parameter">function_name</replaceable></term>
     <term><replaceable class="parameter">operator_name</replaceable></term>
     <term><replaceable class="parameter">policy_name</replaceable></term>
+    <term><replaceable class="parameter">procedure_name</replaceable></term>
+    <term><replaceable class="parameter">routine_name</replaceable></term>
     <term><replaceable class="parameter">rule_name</replaceable></term>
     <term><replaceable class="parameter">trigger_name</replaceable></term>
     <listitem>
      <para>
       The name of the object to be commented.  Names of tables,
       aggregates, collations, conversions, domains, foreign tables, functions,
-      indexes, operators, operator classes, operator families, sequences,
+      indexes, operators, operator classes, operator families, procedures, routines, sequences,
       statistics, text search objects, types, and views can be
       schema-qualified. When commenting on a column,
       <replaceable class="parameter">relation_name</replaceable> must refer
@@ -170,7 +174,7 @@ <title>Parameters</title>
     <term><replaceable class="parameter">argmode</replaceable></term>
     <listitem>
      <para>
-      The mode of a function or aggregate
+      The mode of a function, procedure, or aggregate
       argument: <literal>IN</literal>, <literal>OUT</literal>,
       <literal>INOUT</literal>, or <literal>VARIADIC</literal>.
       If omitted, the default is <literal>IN</literal>.
@@ -187,7 +191,7 @@ <title>Parameters</title>
     <term><replaceable class="parameter">argname</replaceable></term>
     <listitem>
      <para>
-      The name of a function or aggregate argument.
+      The name of a function, procedure, or aggregate argument.
       Note that <command>COMMENT</command> does not actually pay
       any attention to argument names, since only the argument data
       types are needed to determine the function's identity.
@@ -199,7 +203,7 @@ <title>Parameters</title>
     <term><replaceable class="parameter">argtype</replaceable></term>
     <listitem>
      <para>
-      The data type of a function or aggregate argument.
+      The data type of a function, procedure, or aggregate argument.
      </para>
     </listitem>
    </varlistentry>
@@ -325,6 +329,7 @@ <title>Examples</title>
 COMMENT ON OPERATOR CLASS int4ops USING btree IS '4 byte integer operators for btrees';
 COMMENT ON OPERATOR FAMILY integer_ops USING btree IS 'all integer operators for btrees';
 COMMENT ON POLICY my_policy ON mytable IS 'Filter rows by users';
+COMMENT ON PROCEDURE my_proc (integer, integer) IS 'Runs a report';
 COMMENT ON ROLE my_role IS 'Administration group for finance tables';
 COMMENT ON RULE my_rule ON my_table IS 'Logs updates of employee records';
 COMMENT ON SCHEMA my_schema IS 'Departmental data';
diff --git a/doc/src/sgml/ref/create_function.sgml b/doc/src/sgml/ref/create_function.sgml
index 970dc13359..55fa65851f 100644
--- a/doc/src/sgml/ref/create_function.sgml
+++ b/doc/src/sgml/ref/create_function.sgml
@@ -450,7 +450,7 @@ <title>Parameters</title>
    </varlistentry>
 
     <varlistentry>
-     <term><replaceable class="parameter">execution_cost</replaceable></term>
+     <term><literal>COST</literal> <replaceable class="parameter">execution_cost</replaceable></term>
 
      <listitem>
       <para>
@@ -466,7 +466,7 @@ <title>Parameters</title>
     </varlistentry>
 
     <varlistentry>
-     <term><replaceable class="parameter">result_rows</replaceable></term>
+     <term><literal>ROWS</literal> <replaceable class="parameter">result_rows</replaceable></term>
 
      <listitem>
       <para>
diff --git a/doc/src/sgml/ref/create_procedure.sgml b/doc/src/sgml/ref/create_procedure.sgml
new file mode 100644
index 0000000000..f1377f19ad
--- /dev/null
+++ b/doc/src/sgml/ref/create_procedure.sgml
@@ -0,0 +1,374 @@
+<!--
+doc/src/sgml/ref/create_procedure.sgml
+-->
+
+<refentry id="sql-createprocedure">
+ <indexterm zone="sql-createprocedure">
+  <primary>CREATE PROCEDURE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE PROCEDURE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE PROCEDURE</refname>
+  <refpurpose>define a new procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE [ OR REPLACE ] PROCEDURE
+    <replaceable class="parameter">name</replaceable> ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [ { DEFAULT | = } <replaceable class="parameter">default_expr</replaceable> ] [, ...] ] )
+  { LANGUAGE <replaceable class="parameter">lang_name</replaceable>
+    | TRANSFORM { FOR TYPE <replaceable class="parameter">type_name</replaceable> } [, ... ]
+    | IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF
+    | [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
+    | PARALLEL { UNSAFE | RESTRICTED | SAFE }
+    | COST <replaceable class="parameter">execution_cost</replaceable>
+    | ROWS <replaceable class="parameter">result_rows</replaceable>
+    | SET <replaceable class="parameter">configuration_parameter</replaceable> { TO <replaceable class="parameter">value</replaceable> | = <replaceable class="parameter">value</replaceable> | FROM CURRENT }
+    | AS '<replaceable class="parameter">definition</replaceable>'
+    | AS '<replaceable class="parameter">obj_file</replaceable>', '<replaceable class="parameter">link_symbol</replaceable>'
+  } ...
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="sql-createprocedure-description">
+  <title>Description</title>
+
+  <para>
+   <command>CREATE PROCEDURE</command> defines a new procedure.
+   <command>CREATE OR REPLACE PROCEDURE</command> will either create a
+   new procedure, or replace an existing definition.
+   To be able to define a procedure, the user must have the
+   <literal>USAGE</literal> privilege on the language.
+  </para>
+
+  <para>
+   If a schema name is included, then the procedure is created in the
+   specified schema.  Otherwise it is created in the current schema.
+   The name of the new procedure must not match any existing procedure
+   with the same input argument types in the same schema.  However,
+   procedures of different argument types can share a name (this is
+   called <firstterm>overloading</firstterm>).
+  </para>
+
+  <para>
+   To replace the current definition of an existing procedure, use
+   <command>CREATE OR REPLACE PROCEDURE</command>.  It is not possible
+   to change the name or argument types of a procedure this way (if you
+   tried, you would actually be creating a new, distinct procedure).
+  </para>
+
+  <para>
+   When <command>CREATE OR REPLACE PROCEDURE</command> is used to replace an
+   existing procedure, the ownership and permissions of the procedure
+   do not change.  All other procedure properties are assigned the
+   values specified or implied in the command.  You must own the procedure
+   to replace it (this includes being a member of the owning role).
+  </para>
+
+  <para>
+   The user that creates the procedure becomes the owner of the procedure.
+  </para>
+
+  <para>
+   To be able to create a procedure, you must have <literal>USAGE</literal>
+   privilege on the argument types.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><replaceable class="parameter">name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the procedure to create.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">argmode</replaceable></term>
+
+     <listitem>
+      <para>
+       The mode of an argument: <literal>IN</literal> or <literal>VARIADIC</literal>.
+       If omitted, the default is <literal>IN</literal>.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">argname</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of an argument.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">argtype</replaceable></term>
+
+     <listitem>
+      <para>
+       The data type(s) of the procedure's arguments (optionally
+       schema-qualified), if any. The argument types can be base, composite,
+       or domain types, or can reference the type of a table column.
+      </para>
+      <para>
+       Depending on the implementation language it might also be allowed
+       to specify <quote>pseudo-types</quote> such as <type>cstring</type>.
+       Pseudo-types indicate that the actual argument type is either
+       incompletely specified, or outside the set of ordinary SQL data types.
+      </para>
+      <para>
+       The type of a column is referenced by writing
+       <literal><replaceable
+       class="parameter">table_name</replaceable>.<replaceable
+       class="parameter">column_name</replaceable>%TYPE</literal>.
+       Using this feature can sometimes help make a procedure independent of
+       changes to the definition of a table.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">default_expr</replaceable></term>
+
+     <listitem>
+      <para>
+       An expression to be used as default value if the parameter is
+       not specified.  The expression has to be coercible to the
+       argument type of the parameter.
+       All input parameters following a
+       parameter with a default value must have default values as well.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">lang_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the language that the procedure is implemented in.
+       It can be <literal>sql</literal>, <literal>c</literal>,
+       <literal>internal</literal>, or the name of a user-defined
+       procedural language, e.g. <literal>plpgsql</literal>.  Enclosing the
+       name in single quotes is deprecated and requires matching case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>TRANSFORM { FOR TYPE <replaceable class="parameter">type_name</replaceable> } [, ... ] }</literal></term>
+
+     <listitem>
+      <para>
+       Lists which transforms a call to the procedure should apply.  Transforms
+       convert between SQL types and language-specific data types;
+       see <xref linkend="sql-createtransform">.  Procedural language
+       implementations usually have hardcoded knowledge of the built-in types,
+       so those don't need to be listed here.  If a procedural language
+       implementation does not know how to handle a type and no transform is
+       supplied, it will fall back to a default behavior for converting data
+       types, but this depends on the implementation.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>IMMUTABLE</literal></term>
+     <term><literal>STABLE</literal></term>
+     <term><literal>VOLATILE</literal></term>
+     <term><literal>LEAKPROOF</literal></term>
+     <term><literal>PARALLEL</literal></term>
+     <term><literal>COST</literal> <replaceable class="parameter">execution_cost</replaceable></term>
+     <term><literal>ROWS</literal> <replaceable class="parameter">result_rows</replaceable></term>
+
+     <listitem>
+      <para>
+       These attributes are accepted for compatibility
+       with <xref linkend="sql-createfunction"> but are currently not used for
+       procedures.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   <varlistentry>
+    <term><literal><optional>EXTERNAL</optional> SECURITY INVOKER</literal></term>
+    <term><literal><optional>EXTERNAL</optional> SECURITY DEFINER</literal></term>
+
+    <listitem>
+     <para><literal>SECURITY INVOKER</literal> indicates that the procedure
+      is to be executed with the privileges of the user that calls it.
+      That is the default.  <literal>SECURITY DEFINER</literal>
+      specifies that the procedure is to be executed with the
+      privileges of the user that owns it.
+     </para>
+
+     <para>
+      The key word <literal>EXTERNAL</literal> is allowed for SQL
+      conformance, but it is optional since, unlike in SQL, this feature
+      applies to all procedures not only external ones.
+     </para>
+    </listitem>
+   </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>configuration_parameter</replaceable></term>
+     <term><replaceable>value</replaceable></term>
+     <listitem>
+      <para>
+       The <literal>SET</literal> clause causes the specified configuration
+       parameter to be set to the specified value when the procedure is
+       entered, and then restored to its prior value when the procedure exits.
+       <literal>SET FROM CURRENT</literal> saves the value of the parameter that
+       is current when <command>CREATE PROCEDURE</command> is executed as the value
+       to be applied when the procedure is entered.
+      </para>
+
+      <para>
+       If a <literal>SET</literal> clause is attached to a procedure, then
+       the effects of a <command>SET LOCAL</command> command executed inside the
+       procedure for the same variable are restricted to the procedure: the
+       configuration parameter's prior value is still restored at procedure exit.
+       However, an ordinary
+       <command>SET</command> command (without <literal>LOCAL</literal>) overrides the
+       <literal>SET</literal> clause, much as it would do for a previous <command>SET
+       LOCAL</command> command: the effects of such a command will persist after
+       procedure exit, unless the current transaction is rolled back.
+      </para>
+
+      <para>
+       See <xref linkend="sql-set"> and
+       <xref linkend="runtime-config">
+       for more information about allowed parameter names and values.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">definition</replaceable></term>
+
+     <listitem>
+      <para>
+       A string constant defining the procedure; the meaning depends on the
+       language.  It can be an internal procedure name, the path to an
+       object file, an SQL command, or text in a procedural language.
+      </para>
+
+      <para>
+       It is often helpful to use dollar quoting (see <xref
+       linkend="sql-syntax-dollar-quoting">) to write the procedure definition
+       string, rather than the normal single quote syntax.  Without dollar
+       quoting, any single quotes or backslashes in the procedure definition must
+       be escaped by doubling them.
+      </para>
+
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal><replaceable class="parameter">obj_file</replaceable>, <replaceable class="parameter">link_symbol</replaceable></literal></term>
+
+     <listitem>
+      <para>
+       This form of the <literal>AS</literal> clause is used for
+       dynamically loadable C language procedures when the procedure name
+       in the C language source code is not the same as the name of
+       the SQL procedure. The string <replaceable
+       class="parameter">obj_file</replaceable> is the name of the shared
+       library file containing the compiled C procedure, and is interpreted
+       as for the <xref linkend="SQL-LOAD"> command.  The string
+       <replaceable class="parameter">link_symbol</replaceable> is the
+       procedure's link symbol, that is, the name of the procedure in the C
+       language source code.  If the link symbol is omitted, it is assumed
+       to be the same as the name of the SQL procedure being defined.
+      </para>
+
+      <para>
+       When repeated <command>CREATE PROCEDURE</command> calls refer to
+       the same object file, the file is only loaded once per session.
+       To unload and
+       reload the file (perhaps during development), start a new session.
+      </para>
+
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-createprocedure-notes">
+  <title>Notes</title>
+
+  <para>
+   See <xref linkend="sql-createfunction"> for more details on function
+   creation that also apply to procedures.
+  </para>
+
+  <para>
+   Use <xref linkend="sql-call"> to execute a procedure.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-createprocedure-examples">
+  <title>Examples</title>
+
+<programlisting>
+CREATE PROCEDURE insert_data(a integer, b integer)
+LANGUAGE SQL
+AS $$
+INSERT INTO tbl VALUES (a);
+INSERT INTO tbl VALUES (b);
+$$;
+</programlisting>
+ </refsect1>
+
+ <refsect1 id="sql-createprocedure-compat">
+  <title>Compatibility</title>
+
+  <para>
+   A <command>CREATE PROCEDURE</command> command is defined in SQL:1999 and later.
+   The <productname>PostgreSQL</productname> version is similar but
+   not fully compatible.  The attributes are not portable, neither are the
+   different available languages.
+  </para>
+
+  <para>
+   For compatibility with some other database systems,
+   <replaceable class="parameter">argmode</replaceable> can be written
+   either before or after <replaceable class="parameter">argname</replaceable>.
+   But only the first way is standard-compliant.
+  </para>
+
+  <para>
+   For parameter defaults, the SQL standard specifies only the syntax with
+   the <literal>DEFAULT</literal> key word.  The syntax
+   with <literal>=</literal> is used in T-SQL and Firebird.
+  </para>
+ </refsect1>
+
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterprocedure"></member>
+   <member><xref linkend="sql-dropprocedure"></member>
+   <member><xref linkend="sql-call"></member>
+   <member><xref linkend="sql-createfunction"></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_procedure.sgml b/doc/src/sgml/ref/drop_procedure.sgml
new file mode 100644
index 0000000000..0f2484f119
--- /dev/null
+++ b/doc/src/sgml/ref/drop_procedure.sgml
@@ -0,0 +1,161 @@
+<!--
+doc/src/sgml/ref/drop_procedure.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropprocedure">
+ <indexterm zone="sql-dropprocedure">
+  <primary>DROP PROCEDURE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP PROCEDURE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP PROCEDURE</refname>
+  <refpurpose>remove a procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP PROCEDURE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] [, ...]
+    [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP PROCEDURE</command> removes the definition of an existing
+   procedure. To execute this command the user must be the
+   owner of the procedure. The argument types to the
+   procedure must be specified, since several different procedures
+   can exist with the same name and different argument lists.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+    <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the procedure does not exist. A notice is issued
+      in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing procedure.  If no
+      argument list is specified, the name must be unique in its schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argmode</replaceable></term>
+
+    <listitem>
+     <para>
+      The mode of an argument: <literal>IN</literal> or <literal>VARIADIC</literal>.
+      If omitted, the default is <literal>IN</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argname</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of an argument.
+      Note that <command>DROP PROCEDURE</command> does not actually pay
+      any attention to argument names, since only the argument data
+      types are needed to determine the procedure's identity.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argtype</replaceable></term>
+
+    <listitem>
+     <para>
+      The data type(s) of the procedure's arguments (optionally
+      schema-qualified), if any.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the procedure,
+      and in turn all objects that depend on those objects
+      (see <xref linkend="ddl-depend">).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the procedure if any objects depend on it.  This
+      is the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-dropprocedure-examples">
+  <title>Examples</title>
+
+<programlisting>
+DROP PROCEDURE do_db_maintenance();
+</programlisting>
+ </refsect1>
+
+ <refsect1 id="sql-dropprocedure-compatibility">
+  <title>Compatibility</title>
+
+  <para>
+   This command conforms to the SQL standard, with
+   these <productname>PostgreSQL</productname> extensions:
+   <itemizedlist>
+    <listitem>
+     <para>The standard only allows one procedure to be dropped per command.</para>
+    </listitem>
+    <listitem>
+     <para>The <literal>IF EXISTS</literal> option</para>
+    </listitem>
+    <listitem>
+     <para>The ability to specify argument modes and names</para>
+    </listitem>
+   </itemizedlist>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createprocedure"></member>
+   <member><xref linkend="sql-alterprocedure"></member>
+   <member><xref linkend="sql-dropfunction"></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_routine.sgml b/doc/src/sgml/ref/drop_routine.sgml
new file mode 100644
index 0000000000..803b7367a2
--- /dev/null
+++ b/doc/src/sgml/ref/drop_routine.sgml
@@ -0,0 +1,90 @@
+<!--
+doc/src/sgml/ref/drop_routine.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-droproutine">
+ <indexterm zone="sql-droproutine">
+  <primary>DROP ROUTINE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP ROUTINE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP ROUTINE</refname>
+  <refpurpose>remove a routine</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP ROUTINE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] [, ...]
+    [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP ROUTINE</command> removes the definition of an existing
+   routine, which can be an aggregate function, a normal function, or a
+   procedure.  See
+   under <xref linkend="sql-dropaggregate">, <xref linkend="sql-dropfunction">,
+   and <xref linkend="sql-dropprocedure"> for the description of the
+   parameters, more examples, and further details.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-droproutine-examples">
+  <title>Examples</title>
+
+  <para>
+   To drop the routine <literal>foo</literal> for type
+   <type>integer</type>:
+<programlisting>
+DROP ROUTINE foo(integer);
+</programlisting>
+   This command will work independent of whether <literal>foo</literal> is an
+   aggregate, function, or procedure.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-droproutine-compatibility">
+  <title>Compatibility</title>
+
+  <para>
+   This command conforms to the SQL standard, with
+   these <productname>PostgreSQL</productname> extensions:
+   <itemizedlist>
+    <listitem>
+     <para>The standard only allows one routine to be dropped per command.</para>
+    </listitem>
+    <listitem>
+     <para>The <literal>IF EXISTS</literal> option</para>
+    </listitem>
+    <listitem>
+     <para>The ability to specify argument modes and names</para>
+    </listitem>
+    <listitem>
+     <para>Aggregate functions are an extension</para>
+    </listitem>
+   </itemizedlist>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-dropaggregate"></member>
+   <member><xref linkend="sql-dropfunction"></member>
+   <member><xref linkend="sql-dropprocedure"></member>
+   <member><xref linkend="sql-alterroutine"></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 475c85b835..ad416644d9 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -55,8 +55,8 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
 
 GRANT { EXECUTE | ALL [ PRIVILEGES ] }
-    ON { FUNCTION <replaceable>function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
-         | ALL FUNCTIONS IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] }
+    ON { { FUNCTION | PROCEDURE | ROUTINE } <replaceable>function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
+         | ALL { FUNCTIONS | PROCEDURES | ROUTINES } IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] }
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
 
 GRANT { USAGE | ALL [ PRIVILEGES ] }
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index e3e3f2ffc3..41e359548b 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -70,8 +70,8 @@
 
 REVOKE [ GRANT OPTION FOR ]
     { EXECUTE | ALL [ PRIVILEGES ] }
-    ON { FUNCTION <replaceable>function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
-         | ALL FUNCTIONS IN SCHEMA <replaceable>schema_name</replaceable> [, ...] }
+    ON { { FUNCTION | PROCEDURE | ROUTINE } <replaceable>function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
+         | ALL { FUNCTIONS | PROCEDURES | ROUTINES } IN SCHEMA <replaceable>schema_name</replaceable> [, ...] }
     FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
     [ CASCADE | RESTRICT ]
 
diff --git a/doc/src/sgml/ref/security_label.sgml b/doc/src/sgml/ref/security_label.sgml
index ce5a1c1975..6ff9e7a6b0 100644
--- a/doc/src/sgml/ref/security_label.sgml
+++ b/doc/src/sgml/ref/security_label.sgml
@@ -34,8 +34,10 @@
   LARGE OBJECT <replaceable class="parameter">large_object_oid</replaceable> |
   MATERIALIZED VIEW <replaceable class="parameter">object_name</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
+  PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   PUBLICATION <replaceable class="parameter">object_name</replaceable> |
   ROLE <replaceable class="parameter">object_name</replaceable> |
+  ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   SCHEMA <replaceable class="parameter">object_name</replaceable> |
   SEQUENCE <replaceable class="parameter">object_name</replaceable> |
   SUBSCRIPTION <replaceable class="parameter">object_name</replaceable> |
@@ -93,10 +95,12 @@ <title>Parameters</title>
     <term><replaceable class="parameter">table_name.column_name</replaceable></term>
     <term><replaceable class="parameter">aggregate_name</replaceable></term>
     <term><replaceable class="parameter">function_name</replaceable></term>
+    <term><replaceable class="parameter">procedure_name</replaceable></term>
+    <term><replaceable class="parameter">routine_name</replaceable></term>
     <listitem>
      <para>
       The name of the object to be labeled.  Names of tables,
-      aggregates, domains, foreign tables, functions, sequences, types, and
+      aggregates, domains, foreign tables, functions, procedures, routines, sequences, types, and
       views can be schema-qualified.
      </para>
     </listitem>
@@ -119,7 +123,7 @@ <title>Parameters</title>
 
     <listitem>
      <para>
-      The mode of a function or aggregate
+      The mode of a function, procedure, or aggregate
       argument: <literal>IN</literal>, <literal>OUT</literal>,
       <literal>INOUT</literal>, or <literal>VARIADIC</literal>.
       If omitted, the default is <literal>IN</literal>.
@@ -137,7 +141,7 @@ <title>Parameters</title>
 
     <listitem>
      <para>
-      The name of a function or aggregate argument.
+      The name of a function, procedure, or aggregate argument.
       Note that <command>SECURITY LABEL</command> does not actually
       pay any attention to argument names, since only the argument data
       types are needed to determine the function's identity.
@@ -150,7 +154,7 @@ <title>Parameters</title>
 
     <listitem>
      <para>
-      The data type of a function or aggregate argument.
+      The data type of a function, procedure, or aggregate argument.
      </para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 9000b3aaaa..52760ca62a 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -54,8 +54,10 @@ <title>SQL Commands</title>
    &alterOperatorClass;
    &alterOperatorFamily;
    &alterPolicy;
+   &alterProcedure;
    &alterPublication;
    &alterRole;
+   &alterRoutine;
    &alterRule;
    &alterSchema;
    &alterSequence;
@@ -76,6 +78,7 @@ <title>SQL Commands</title>
    &alterView;
    &analyze;
    &begin;
+   &call;
    &checkpoint;
    &close;
    &cluster;
@@ -103,6 +106,7 @@ <title>SQL Commands</title>
    &createOperatorClass;
    &createOperatorFamily;
    &createPolicy;
+   &createProcedure;
    &createPublication;
    &createRole;
    &createRule;
@@ -150,8 +154,10 @@ <title>SQL Commands</title>
    &dropOperatorFamily;
    &dropOwned;
    &dropPolicy;
+   &dropProcedure;
    &dropPublication;
    &dropRole;
+   &dropRoutine;
    &dropRule;
    &dropSchema;
    &dropSequence;
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ccde66a7dd..0b9d324918 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -482,6 +482,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_NAMESPACE;
 			errormsg = gettext_noop("invalid privilege type %s for schema");
 			break;
+		case ACL_OBJECT_PROCEDURE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for procedure");
+			break;
+		case ACL_OBJECT_ROUTINE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for routine");
+			break;
 		case ACL_OBJECT_TABLESPACE:
 			all_privileges = ACL_ALL_RIGHTS_TABLESPACE;
 			errormsg = gettext_noop("invalid privilege type %s for tablespace");
@@ -584,6 +592,8 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 			ExecGrant_ForeignServer(istmt);
 			break;
 		case ACL_OBJECT_FUNCTION:
+		case ACL_OBJECT_PROCEDURE:
+		case ACL_OBJECT_ROUTINE:
 			ExecGrant_Function(istmt);
 			break;
 		case ACL_OBJECT_LANGUAGE:
@@ -671,7 +681,7 @@ objectNamesToOids(GrantObjectType objtype, List *objnames)
 				ObjectWithArgs *func = (ObjectWithArgs *) lfirst(cell);
 				Oid			funcid;
 
-				funcid = LookupFuncWithArgs(func, false);
+				funcid = LookupFuncWithArgs(OBJECT_FUNCTION, func, false);
 				objects = lappend_oid(objects, funcid);
 			}
 			break;
@@ -709,6 +719,26 @@ objectNamesToOids(GrantObjectType objtype, List *objnames)
 				objects = lappend_oid(objects, oid);
 			}
 			break;
+		case ACL_OBJECT_PROCEDURE:
+			foreach(cell, objnames)
+			{
+				ObjectWithArgs *func = (ObjectWithArgs *) lfirst(cell);
+				Oid			procid;
+
+				procid = LookupFuncWithArgs(OBJECT_PROCEDURE, func, false);
+				objects = lappend_oid(objects, procid);
+			}
+			break;
+		case ACL_OBJECT_ROUTINE:
+			foreach(cell, objnames)
+			{
+				ObjectWithArgs *func = (ObjectWithArgs *) lfirst(cell);
+				Oid			routid;
+
+				routid = LookupFuncWithArgs(OBJECT_ROUTINE, func, false);
+				objects = lappend_oid(objects, routid);
+			}
+			break;
 		case ACL_OBJECT_TABLESPACE:
 			foreach(cell, objnames)
 			{
@@ -785,8 +815,10 @@ objectsInSchemaToOids(GrantObjectType objtype, List *nspnames)
 				objects = list_concat(objects, objs);
 				break;
 			case ACL_OBJECT_FUNCTION:
+			case ACL_OBJECT_PROCEDURE:
+			case ACL_OBJECT_ROUTINE:
 				{
-					ScanKeyData key[1];
+					ScanKeyData key[2];
 					Relation	rel;
 					HeapScanDesc scan;
 					HeapTuple	tuple;
@@ -795,9 +827,21 @@ objectsInSchemaToOids(GrantObjectType objtype, List *nspnames)
 								Anum_pg_proc_pronamespace,
 								BTEqualStrategyNumber, F_OIDEQ,
 								ObjectIdGetDatum(namespaceId));
+					if (objtype == ACL_OBJECT_FUNCTION)
+						ScanKeyInit(&key[1],
+									Anum_pg_proc_prorettype,
+									BTEqualStrategyNumber, F_OIDNE,
+									InvalidOid);
+					else if (objtype == ACL_OBJECT_PROCEDURE)
+						ScanKeyInit(&key[1],
+									Anum_pg_proc_prorettype,
+									BTEqualStrategyNumber, F_OIDEQ,
+									InvalidOid);
 
 					rel = heap_open(ProcedureRelationId, AccessShareLock);
-					scan = heap_beginscan_catalog(rel, 1, key);
+					scan = heap_beginscan_catalog(rel,
+												  objtype == ACL_OBJECT_ROUTINE ? 1 : 2,
+												  key);
 
 					while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
 					{
@@ -955,6 +999,14 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
 			errormsg = gettext_noop("invalid privilege type %s for function");
 			break;
+		case ACL_OBJECT_PROCEDURE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for procedure");
+			break;
+		case ACL_OBJECT_ROUTINE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for routine");
+			break;
 		case ACL_OBJECT_TYPE:
 			all_privileges = ACL_ALL_RIGHTS_TYPE;
 			errormsg = gettext_noop("invalid privilege type %s for type");
@@ -1423,7 +1475,7 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 				istmt.objtype = ACL_OBJECT_TYPE;
 				break;
 			case ProcedureRelationId:
-				istmt.objtype = ACL_OBJECT_FUNCTION;
+				istmt.objtype = ACL_OBJECT_ROUTINE;
 				break;
 			case LanguageRelationId:
 				istmt.objtype = ACL_OBJECT_LANGUAGE;
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 236f6be37e..360725d59a 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1413,7 +1413,8 @@ CREATE VIEW routines AS
            CAST(current_database() AS sql_identifier) AS routine_catalog,
            CAST(n.nspname AS sql_identifier) AS routine_schema,
            CAST(p.proname AS sql_identifier) AS routine_name,
-           CAST('FUNCTION' AS character_data) AS routine_type,
+           CAST(CASE WHEN p.prorettype <> 0 THEN 'FUNCTION' ELSE 'PROCEDURE' END
+             AS character_data) AS routine_type,
            CAST(null AS sql_identifier) AS module_catalog,
            CAST(null AS sql_identifier) AS module_schema,
            CAST(null AS sql_identifier) AS module_name,
@@ -1422,7 +1423,8 @@ CREATE VIEW routines AS
            CAST(null AS sql_identifier) AS udt_name,
 
            CAST(
-             CASE WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY'
+             CASE WHEN p.prorettype = 0 THEN NULL
+                  WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY'
                   WHEN nt.nspname = 'pg_catalog' THEN format_type(t.oid, null)
                   ELSE 'USER-DEFINED' END AS character_data)
              AS data_type,
@@ -1440,7 +1442,7 @@ CREATE VIEW routines AS
            CAST(null AS cardinal_number) AS datetime_precision,
            CAST(null AS character_data) AS interval_type,
            CAST(null AS cardinal_number) AS interval_precision,
-           CAST(current_database() AS sql_identifier) AS type_udt_catalog,
+           CAST(CASE WHEN p.prorettype <> 0 THEN current_database() END AS sql_identifier) AS type_udt_catalog,
            CAST(nt.nspname AS sql_identifier) AS type_udt_schema,
            CAST(t.typname AS sql_identifier) AS type_udt_name,
            CAST(null AS sql_identifier) AS scope_catalog,
@@ -1462,7 +1464,8 @@ CREATE VIEW routines AS
            CAST('GENERAL' AS character_data) AS parameter_style,
            CAST(CASE WHEN p.provolatile = 'i' THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_deterministic,
            CAST('MODIFIES' AS character_data) AS sql_data_access,
-           CAST(CASE WHEN p.proisstrict THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_null_call,
+           CAST(CASE WHEN p.prorettype <> 0 THEN
+             CASE WHEN p.proisstrict THEN 'YES' ELSE 'NO' END END AS yes_or_no) AS is_null_call,
            CAST(null AS character_data) AS sql_path,
            CAST('YES' AS yes_or_no) AS schema_level_routine,
            CAST(0 AS cardinal_number) AS max_dynamic_result_sets,
@@ -1503,13 +1506,15 @@ CREATE VIEW routines AS
            CAST(null AS cardinal_number) AS result_cast_maximum_cardinality,
            CAST(null AS sql_identifier) AS result_cast_dtd_identifier
 
-    FROM pg_namespace n, pg_proc p, pg_language l,
-         pg_type t, pg_namespace nt
+    FROM (pg_namespace n
+          JOIN pg_proc p ON n.oid = p.pronamespace
+          JOIN pg_language l ON p.prolang = l.oid)
+         LEFT JOIN
+         (pg_type t JOIN pg_namespace nt ON t.typnamespace = nt.oid)
+         ON p.prorettype = t.oid
 
-    WHERE n.oid = p.pronamespace AND p.prolang = l.oid
-          AND p.prorettype = t.oid AND t.typnamespace = nt.oid
-          AND (pg_has_role(p.proowner, 'USAGE')
-               OR has_function_privilege(p.oid, 'EXECUTE'));
+    WHERE (pg_has_role(p.proowner, 'USAGE')
+           OR has_function_privilege(p.oid, 'EXECUTE'));
 
 GRANT SELECT ON routines TO PUBLIC;
 
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index c2ad7c675e..55e23c86be 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -566,6 +566,9 @@ static const struct object_type_map
 	{
 		"function", OBJECT_FUNCTION
 	},
+	{
+		"procedure", OBJECT_PROCEDURE
+	},
 	/* OCLASS_TYPE */
 	{
 		"type", OBJECT_TYPE
@@ -884,13 +887,11 @@ get_object_address(ObjectType objtype, Node *object,
 				address = get_object_address_type(objtype, castNode(TypeName, object), missing_ok);
 				break;
 			case OBJECT_AGGREGATE:
-				address.classId = ProcedureRelationId;
-				address.objectId = LookupAggWithArgs(castNode(ObjectWithArgs, object), missing_ok);
-				address.objectSubId = 0;
-				break;
 			case OBJECT_FUNCTION:
+			case OBJECT_PROCEDURE:
+			case OBJECT_ROUTINE:
 				address.classId = ProcedureRelationId;
-				address.objectId = LookupFuncWithArgs(castNode(ObjectWithArgs, object), missing_ok);
+				address.objectId = LookupFuncWithArgs(objtype, castNode(ObjectWithArgs, object), missing_ok);
 				address.objectSubId = 0;
 				break;
 			case OBJECT_OPERATOR:
@@ -2025,6 +2026,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 	 */
 	if (type == OBJECT_AGGREGATE ||
 		type == OBJECT_FUNCTION ||
+		type == OBJECT_PROCEDURE ||
+		type == OBJECT_ROUTINE ||
 		type == OBJECT_OPERATOR ||
 		type == OBJECT_CAST ||
 		type == OBJECT_AMOP ||
@@ -2168,6 +2171,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_AGGREGATE:
 		case OBJECT_OPERATOR:
 			{
@@ -2253,6 +2258,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AGGREGATE:
 		case OBJECT_FUNCTION:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 			if (!pg_proc_ownercheck(address.objectId, roleid))
 				aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_PROC,
 							   NameListToString((castNode(ObjectWithArgs, object))->objname));
@@ -4026,6 +4033,8 @@ getProcedureTypeDescription(StringInfo buffer, Oid procid)
 
 	if (procForm->proisagg)
 		appendStringInfoString(buffer, "aggregate");
+	else if (procForm->prorettype == InvalidOid)
+		appendStringInfoString(buffer, "procedure");
 	else
 		appendStringInfoString(buffer, "function");
 
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 47916cfb54..7d05e4bdb2 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -857,7 +857,8 @@ fmgr_sql_validator(PG_FUNCTION_ARGS)
 
 	/* Disallow pseudotype result */
 	/* except for RECORD, VOID, or polymorphic */
-	if (get_typtype(proc->prorettype) == TYPTYPE_PSEUDO &&
+	if (proc->prorettype &&
+		get_typtype(proc->prorettype) == TYPTYPE_PSEUDO &&
 		proc->prorettype != RECORDOID &&
 		proc->prorettype != VOIDOID &&
 		!IsPolymorphicType(proc->prorettype))
diff --git a/src/backend/commands/aggregatecmds.c b/src/backend/commands/aggregatecmds.c
index adc9877e79..2e2ee883e2 100644
--- a/src/backend/commands/aggregatecmds.c
+++ b/src/backend/commands/aggregatecmds.c
@@ -307,7 +307,7 @@ DefineAggregate(ParseState *pstate, List *name, List *args, bool oldstyle, List
 		interpret_function_parameter_list(pstate,
 										  args,
 										  InvalidOid,
-										  true, /* is an aggregate */
+										  OBJECT_AGGREGATE,
 										  &parameterTypes,
 										  &allParameterTypes,
 										  &parameterModes,
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 4f8147907c..21e3f1efe1 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -378,6 +378,8 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
 		case OBJECT_LANGUAGE:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TSCONFIGURATION:
 		case OBJECT_TSDICTIONARY:
@@ -495,6 +497,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_OPERATOR:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TSCONFIGURATION:
 		case OBJECT_TSDICTIONARY:
@@ -842,6 +846,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_OPERATOR:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TABLESPACE:
 		case OBJECT_TSDICTIONARY:
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 2b30677d6f..7e6baa1928 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -26,6 +26,7 @@
 #include "nodes/makefuncs.h"
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 
@@ -91,21 +92,12 @@ RemoveObjects(DropStmt *stmt)
 		 */
 		if (stmt->removeType == OBJECT_FUNCTION)
 		{
-			Oid			funcOid = address.objectId;
-			HeapTuple	tup;
-
-			tup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcOid));
-			if (!HeapTupleIsValid(tup)) /* should not happen */
-				elog(ERROR, "cache lookup failed for function %u", funcOid);
-
-			if (((Form_pg_proc) GETSTRUCT(tup))->proisagg)
+			if (get_func_isagg(address.objectId))
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 						 errmsg("\"%s\" is an aggregate function",
 								NameListToString(castNode(ObjectWithArgs, object)->objname)),
 						 errhint("Use DROP AGGREGATE to drop aggregate functions.")));
-
-			ReleaseSysCache(tup);
 		}
 
 		/* Check permissions. */
@@ -338,6 +330,32 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				}
 				break;
 			}
+		case OBJECT_PROCEDURE:
+			{
+				ObjectWithArgs *owa = castNode(ObjectWithArgs, object);
+
+				if (!schema_does_not_exist_skipping(owa->objname, &msg, &name) &&
+					!type_in_list_does_not_exist_skipping(owa->objargs, &msg, &name))
+				{
+					msg = gettext_noop("procedure %s(%s) does not exist, skipping");
+					name = NameListToString(owa->objname);
+					args = TypeNameListToString(owa->objargs);
+				}
+				break;
+			}
+		case OBJECT_ROUTINE:
+			{
+				ObjectWithArgs *owa = castNode(ObjectWithArgs, object);
+
+				if (!schema_does_not_exist_skipping(owa->objname, &msg, &name) &&
+					!type_in_list_does_not_exist_skipping(owa->objargs, &msg, &name))
+				{
+					msg = gettext_noop("routine %s(%s) does not exist, skipping");
+					name = NameListToString(owa->objname);
+					args = TypeNameListToString(owa->objargs);
+				}
+				break;
+			}
 		case OBJECT_AGGREGATE:
 			{
 				ObjectWithArgs *owa = castNode(ObjectWithArgs, object);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 938133bbe4..988a04ceae 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -106,8 +106,10 @@ static event_trigger_support_data event_trigger_support[] = {
 	{"OPERATOR CLASS", true},
 	{"OPERATOR FAMILY", true},
 	{"POLICY", true},
+	{"PROCEDURE", true},
 	{"PUBLICATION", true},
 	{"ROLE", false},
+	{"ROUTINE", true},
 	{"RULE", true},
 	{"SCHEMA", true},
 	{"SEQUENCE", true},
@@ -1103,8 +1105,10 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_OPERATOR:
 		case OBJECT_OPFAMILY:
 		case OBJECT_POLICY:
+		case OBJECT_PROCEDURE:
 		case OBJECT_PUBLICATION:
 		case OBJECT_PUBLICATION_REL:
+		case OBJECT_ROUTINE:
 		case OBJECT_RULE:
 		case OBJECT_SCHEMA:
 		case OBJECT_SEQUENCE:
@@ -1215,6 +1219,8 @@ EventTriggerSupportsGrantObjectType(GrantObjectType objtype)
 		case ACL_OBJECT_LANGUAGE:
 		case ACL_OBJECT_LARGEOBJECT:
 		case ACL_OBJECT_NAMESPACE:
+		case ACL_OBJECT_PROCEDURE:
+		case ACL_OBJECT_ROUTINE:
 		case ACL_OBJECT_TYPE:
 			return true;
 
@@ -2243,6 +2249,10 @@ stringify_grantobjtype(GrantObjectType objtype)
 			return "LARGE OBJECT";
 		case ACL_OBJECT_NAMESPACE:
 			return "SCHEMA";
+		case ACL_OBJECT_PROCEDURE:
+			return "PROCEDURE";
+		case ACL_OBJECT_ROUTINE:
+			return "ROUTINE";
 		case ACL_OBJECT_TABLESPACE:
 			return "TABLESPACE";
 		case ACL_OBJECT_TYPE:
@@ -2285,6 +2295,10 @@ stringify_adefprivs_objtype(GrantObjectType objtype)
 			return "LARGE OBJECTS";
 		case ACL_OBJECT_NAMESPACE:
 			return "SCHEMAS";
+		case ACL_OBJECT_PROCEDURE:
+			return "PROCEDURES";
+		case ACL_OBJECT_ROUTINE:
+			return "ROUTINES";
 		case ACL_OBJECT_TABLESPACE:
 			return "TABLESPACES";
 		case ACL_OBJECT_TYPE:
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index 7de844b2ca..1f3156d870 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -51,6 +51,8 @@
 #include "commands/alter.h"
 #include "commands/defrem.h"
 #include "commands/proclang.h"
+#include "executor/execdesc.h"
+#include "executor/executor.h"
 #include "miscadmin.h"
 #include "optimizer/var.h"
 #include "parser/parse_coerce.h"
@@ -179,7 +181,7 @@ void
 interpret_function_parameter_list(ParseState *pstate,
 								  List *parameters,
 								  Oid languageOid,
-								  bool is_aggregate,
+								  ObjectType objtype,
 								  oidvector **parameterTypes,
 								  ArrayType **allParameterTypes,
 								  ArrayType **parameterModes,
@@ -233,7 +235,7 @@ interpret_function_parameter_list(ParseState *pstate,
 							 errmsg("SQL function cannot accept shell type %s",
 									TypeNameToString(t))));
 				/* We don't allow creating aggregates on shell types either */
-				else if (is_aggregate)
+				else if (objtype == OBJECT_AGGREGATE)
 					ereport(ERROR,
 							(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 							 errmsg("aggregate cannot accept shell type %s",
@@ -262,16 +264,28 @@ interpret_function_parameter_list(ParseState *pstate,
 
 		if (t->setof)
 		{
-			if (is_aggregate)
+			if (objtype == OBJECT_AGGREGATE)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 						 errmsg("aggregates cannot accept set arguments")));
+			else if (objtype == OBJECT_PROCEDURE)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+						 errmsg("procedures cannot accept set arguments")));
 			else
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 						 errmsg("functions cannot accept set arguments")));
 		}
 
+		if (objtype == OBJECT_PROCEDURE)
+		{
+			if (fp->mode == FUNC_PARAM_OUT || fp->mode == FUNC_PARAM_INOUT)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 (errmsg("procedures cannot have OUT parameters"))));
+		}
+
 		/* handle input parameters */
 		if (fp->mode != FUNC_PARAM_OUT && fp->mode != FUNC_PARAM_TABLE)
 		{
@@ -990,7 +1004,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 	interpret_function_parameter_list(pstate,
 									  stmt->parameters,
 									  languageOid,
-									  false,	/* not an aggregate */
+									  stmt->is_procedure ? OBJECT_PROCEDURE : OBJECT_FUNCTION,
 									  &parameterTypes,
 									  &allParameterTypes,
 									  &parameterModes,
@@ -999,7 +1013,24 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 									  &variadicArgType,
 									  &requiredResultType);
 
-	if (stmt->returnType)
+	if (stmt->is_procedure)
+	{
+		Assert(!stmt->returnType);
+
+		prorettype = InvalidOid;
+		returnsSet = false;
+
+		if (isStrict)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+					 errmsg("procedure cannot be declared as strict")));
+
+		if (isWindowFunc)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+					 errmsg("procedure cannot have WINDOW attribute")));
+	}
+	else if (stmt->returnType)
 	{
 		/* explicit RETURNS clause */
 		compute_return_type(stmt->returnType, languageOid,
@@ -1182,7 +1213,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 
 	rel = heap_open(ProcedureRelationId, RowExclusiveLock);
 
-	funcOid = LookupFuncWithArgs(stmt->func, false);
+	funcOid = LookupFuncWithArgs(stmt->objtype, stmt->func, false);
 
 	tup = SearchSysCacheCopy1(PROCOID, ObjectIdGetDatum(funcOid));
 	if (!HeapTupleIsValid(tup)) /* should not happen */
@@ -1219,6 +1250,11 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 			elog(ERROR, "option \"%s\" not recognized", defel->defname);
 	}
 
+	if (procForm->prorettype == InvalidOid && strict_item)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+				 errmsg("procedure strictness cannot be changed")));
+
 	if (volatility_item)
 		procForm->provolatile = interpret_func_volatility(volatility_item);
 	if (strict_item)
@@ -1472,7 +1508,7 @@ CreateCast(CreateCastStmt *stmt)
 	{
 		Form_pg_proc procstruct;
 
-		funcid = LookupFuncWithArgs(stmt->func, false);
+		funcid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->func, false);
 
 		tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
 		if (!HeapTupleIsValid(tuple))
@@ -1853,7 +1889,7 @@ CreateTransform(CreateTransformStmt *stmt)
 	 */
 	if (stmt->fromsql)
 	{
-		fromsqlfuncid = LookupFuncWithArgs(stmt->fromsql, false);
+		fromsqlfuncid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->fromsql, false);
 
 		if (!pg_proc_ownercheck(fromsqlfuncid, GetUserId()))
 			aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_PROC, NameListToString(stmt->fromsql->objname));
@@ -1879,7 +1915,7 @@ CreateTransform(CreateTransformStmt *stmt)
 
 	if (stmt->tosql)
 	{
-		tosqlfuncid = LookupFuncWithArgs(stmt->tosql, false);
+		tosqlfuncid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->tosql, false);
 
 		if (!pg_proc_ownercheck(tosqlfuncid, GetUserId()))
 			aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_PROC, NameListToString(stmt->tosql->objname));
@@ -2168,3 +2204,80 @@ ExecuteDoStmt(DoStmt *stmt)
 	/* execute the inline handler */
 	OidFunctionCall1(laninline, PointerGetDatum(codeblock));
 }
+
+/*
+ * Execute CALL statement
+ */
+void
+ExecuteCallStmt(ParseState *pstate, CallStmt *stmt)
+{
+	List	   *targs;
+	ListCell   *lc;
+	Node	   *node;
+	FuncExpr   *fexpr;
+	int			nargs;
+	int			i;
+	AclResult   aclresult;
+	FmgrInfo	flinfo;
+	FunctionCallInfoData fcinfo;
+
+	targs = NIL;
+	foreach(lc, stmt->funccall->args)
+	{
+		targs = lappend(targs, transformExpr(pstate,
+											 (Node *) lfirst(lc),
+											 EXPR_KIND_CALL));
+	}
+
+	node = ParseFuncOrColumn(pstate,
+							 stmt->funccall->funcname,
+							 targs,
+							 pstate->p_last_srf,
+							 stmt->funccall,
+							 true,
+							 stmt->funccall->location);
+
+	fexpr = castNode(FuncExpr, node);
+
+	aclresult = pg_proc_aclcheck(fexpr->funcid, GetUserId(), ACL_EXECUTE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, ACL_KIND_PROC, get_func_name(fexpr->funcid));
+	InvokeFunctionExecuteHook(fexpr->funcid);
+
+	nargs = list_length(fexpr->args);
+
+	/* safety check; see ExecInitFunc() */
+	if (nargs > FUNC_MAX_ARGS)
+		ereport(ERROR,
+				(errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+				 errmsg_plural("cannot pass more than %d argument to a procedure",
+							   "cannot pass more than %d arguments to a procedure",
+							   FUNC_MAX_ARGS,
+							   FUNC_MAX_ARGS)));
+
+	fmgr_info(fexpr->funcid, &flinfo);
+	InitFunctionCallInfoData(fcinfo, &flinfo, nargs, fexpr->inputcollid, NULL, NULL);
+
+	i = 0;
+	foreach (lc, fexpr->args)
+	{
+		EState	   *estate;
+		ExprState  *exprstate;
+		ExprContext *econtext;
+		Datum		val;
+		bool		isnull;
+
+		estate = CreateExecutorState();
+		exprstate = ExecPrepareExpr(lfirst(lc), estate);
+		econtext = CreateStandaloneExprContext();
+		val = ExecEvalExprSwitchContext(exprstate, econtext, &isnull);
+		FreeExecutorState(estate);
+
+		fcinfo.arg[i] = val;
+		fcinfo.argnull[i] = isnull;
+
+		i++;
+	}
+
+	FunctionCallInvoke(&fcinfo);
+}
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index d23e6d6f25..76d500462f 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -520,7 +520,7 @@ DefineOpClass(CreateOpClassStmt *stmt)
 							 errmsg("invalid procedure number %d,"
 									" must be between 1 and %d",
 									item->number, maxProcNumber)));
-				funcOid = LookupFuncWithArgs(item->name, false);
+				funcOid = LookupFuncWithArgs(OBJECT_FUNCTION, item->name, false);
 #ifdef NOT_USED
 				/* XXX this is unnecessary given the superuser check above */
 				/* Caller must own function */
@@ -894,7 +894,7 @@ AlterOpFamilyAdd(AlterOpFamilyStmt *stmt, Oid amoid, Oid opfamilyoid,
 							 errmsg("invalid procedure number %d,"
 									" must be between 1 and %d",
 									item->number, maxProcNumber)));
-				funcOid = LookupFuncWithArgs(item->name, false);
+				funcOid = LookupFuncWithArgs(OBJECT_FUNCTION, item->name, false);
 #ifdef NOT_USED
 				/* XXX this is unnecessary given the superuser check above */
 				/* Caller must own function */
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 98eb777421..564ccb004f 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -390,6 +390,7 @@ sql_fn_post_column_ref(ParseState *pstate, ColumnRef *cref, Node *var)
 								  list_make1(param),
 								  pstate->p_last_srf,
 								  NULL,
+								  false,
 								  cref->location);
 	}
 
@@ -658,7 +659,8 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK)
 	fcache->rettype = rettype;
 
 	/* Fetch the typlen and byval info for the result type */
-	get_typlenbyval(rettype, &fcache->typlen, &fcache->typbyval);
+	if (rettype)
+		get_typlenbyval(rettype, &fcache->typlen, &fcache->typbyval);
 
 	/* Remember whether we're returning setof something */
 	fcache->returnsSet = procedureStruct->proretset;
@@ -1322,7 +1324,7 @@ fmgr_sql(PG_FUNCTION_ARGS)
 		else
 		{
 			/* Should only get here for VOID functions */
-			Assert(fcache->rettype == VOIDOID);
+			Assert(fcache->rettype == InvalidOid || fcache->rettype == VOIDOID);
 			fcinfo->isnull = true;
 			result = (Datum) 0;
 		}
@@ -1546,7 +1548,10 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
 	if (modifyTargetList)
 		*modifyTargetList = false;	/* initialize for no change */
 	if (junkFilter)
-		*junkFilter = NULL;		/* initialize in case of VOID result */
+		*junkFilter = NULL;		/* initialize in case of procedure/VOID result */
+
+	if (!rettype)
+		return false;
 
 	/*
 	 * Find the last canSetTag query in the list.  This isn't necessarily the
@@ -1591,7 +1596,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
 	else
 	{
 		/* Empty function body, or last statement is a utility command */
-		if (rettype != VOIDOID)
+		if (rettype && rettype != VOIDOID)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 					 errmsg("return type mismatch in function declared to return %s",
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c1a83ca909..7acee32256 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3208,6 +3208,16 @@ _copyClosePortalStmt(const ClosePortalStmt *from)
 	return newnode;
 }
 
+static CallStmt *
+_copyCallStmt(const CallStmt *from)
+{
+	CallStmt *newnode = makeNode(CallStmt);
+
+	COPY_NODE_FIELD(funccall);
+
+	return newnode;
+}
+
 static ClusterStmt *
 _copyClusterStmt(const ClusterStmt *from)
 {
@@ -3409,6 +3419,7 @@ _copyCreateFunctionStmt(const CreateFunctionStmt *from)
 	COPY_NODE_FIELD(funcname);
 	COPY_NODE_FIELD(parameters);
 	COPY_NODE_FIELD(returnType);
+	COPY_SCALAR_FIELD(is_procedure);
 	COPY_NODE_FIELD(options);
 	COPY_NODE_FIELD(withClause);
 
@@ -3433,6 +3444,7 @@ _copyAlterFunctionStmt(const AlterFunctionStmt *from)
 {
 	AlterFunctionStmt *newnode = makeNode(AlterFunctionStmt);
 
+	COPY_SCALAR_FIELD(objtype);
 	COPY_NODE_FIELD(func);
 	COPY_NODE_FIELD(actions);
 
@@ -5100,6 +5112,9 @@ copyObjectImpl(const void *from)
 		case T_ClosePortalStmt:
 			retval = _copyClosePortalStmt(from);
 			break;
+		case T_CallStmt:
+			retval = _copyCallStmt(from);
+			break;
 		case T_ClusterStmt:
 			retval = _copyClusterStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 7a700018e7..134a974373 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1201,6 +1201,14 @@ _equalClosePortalStmt(const ClosePortalStmt *a, const ClosePortalStmt *b)
 	return true;
 }
 
+static bool
+_equalCallStmt(const CallStmt *a, const CallStmt *b)
+{
+	COMPARE_NODE_FIELD(funccall);
+
+	return true;
+}
+
 static bool
 _equalClusterStmt(const ClusterStmt *a, const ClusterStmt *b)
 {
@@ -1364,6 +1372,7 @@ _equalCreateFunctionStmt(const CreateFunctionStmt *a, const CreateFunctionStmt *
 	COMPARE_NODE_FIELD(funcname);
 	COMPARE_NODE_FIELD(parameters);
 	COMPARE_NODE_FIELD(returnType);
+	COMPARE_SCALAR_FIELD(is_procedure);
 	COMPARE_NODE_FIELD(options);
 	COMPARE_NODE_FIELD(withClause);
 
@@ -1384,6 +1393,7 @@ _equalFunctionParameter(const FunctionParameter *a, const FunctionParameter *b)
 static bool
 _equalAlterFunctionStmt(const AlterFunctionStmt *a, const AlterFunctionStmt *b)
 {
+	COMPARE_SCALAR_FIELD(objtype);
 	COMPARE_NODE_FIELD(func);
 	COMPARE_NODE_FIELD(actions);
 
@@ -3244,6 +3254,9 @@ equal(const void *a, const void *b)
 		case T_ClosePortalStmt:
 			retval = _equalClosePortalStmt(a, b);
 			break;
+		case T_CallStmt:
+			retval = _equalCallStmt(a, b);
+			break;
 		case T_ClusterStmt:
 			retval = _equalClusterStmt(a, b);
 			break;
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 652843a146..d0855e6287 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4379,6 +4379,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 	if (funcform->prolang != SQLlanguageId ||
 		funcform->prosecdef ||
 		funcform->proretset ||
+		funcform->prorettype == InvalidOid ||
 		funcform->prorettype == RECORDOID ||
 		!heap_attisnull(func_tuple, Anum_pg_proc_proconfig) ||
 		funcform->pronargs != list_length(args))
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4c83a63f7d..bf460ef83b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -253,7 +253,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		AlterCompositeTypeStmt AlterUserMappingStmt
 		AlterRoleStmt AlterRoleSetStmt AlterPolicyStmt
 		AlterDefaultPrivilegesStmt DefACLAction
-		AnalyzeStmt ClosePortalStmt ClusterStmt CommentStmt
+		AnalyzeStmt CallStmt ClosePortalStmt ClusterStmt CommentStmt
 		ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt
 		CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
@@ -610,7 +610,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	BACKWARD BEFORE BEGIN_P BETWEEN BIGINT BINARY BIT
 	BOOLEAN_P BOTH BY
 
-	CACHE CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
+	CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
 	CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
 	CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
 	COMMITTED CONCURRENTLY CONFIGURATION CONFLICT CONNECTION CONSTRAINT
@@ -659,14 +659,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
-	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROGRAM PUBLICATION
+	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
 	QUOTE
 
 	RANGE READ REAL REASSIGN RECHECK RECURSIVE REF REFERENCES REFERENCING
 	REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA
 	RESET RESTART RESTRICT RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP
-	ROW ROWS RULE
+	ROUTINE ROUTINES ROW ROWS RULE
 
 	SAVEPOINT SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
 	SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
@@ -844,6 +844,7 @@ stmt :
 			| AlterTSDictionaryStmt
 			| AlterUserMappingStmt
 			| AnalyzeStmt
+			| CallStmt
 			| CheckPointStmt
 			| ClosePortalStmt
 			| ClusterStmt
@@ -939,6 +940,20 @@ stmt :
 				{ $$ = NULL; }
 		;
 
+/*****************************************************************************
+ *
+ * CALL statement
+ *
+ *****************************************************************************/
+
+CallStmt:	CALL func_application
+				{
+					CallStmt *n = makeNode(CallStmt);
+					n->funccall = castNode(FuncCall, $2);
+					$$ = (Node *)n;
+				}
+		;
+
 /*****************************************************************************
  *
  * Create a new Postgres DBMS role
@@ -4482,6 +4497,24 @@ AlterExtensionContentsStmt:
 					n->object = (Node *) lcons(makeString($9), $7);
 					$$ = (Node *)n;
 				}
+			| ALTER EXTENSION name add_drop PROCEDURE function_with_argtypes
+				{
+					AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
+					n->extname = $3;
+					n->action = $4;
+					n->objtype = OBJECT_PROCEDURE;
+					n->object = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			| ALTER EXTENSION name add_drop ROUTINE function_with_argtypes
+				{
+					AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
+					n->extname = $3;
+					n->action = $4;
+					n->objtype = OBJECT_ROUTINE;
+					n->object = (Node *) $6;
+					$$ = (Node *)n;
+				}
 			| ALTER EXTENSION name add_drop SCHEMA name
 				{
 					AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
@@ -6364,6 +6397,22 @@ CommentStmt:
 					n->comment = $8;
 					$$ = (Node *) n;
 				}
+			| COMMENT ON PROCEDURE function_with_argtypes IS comment_text
+				{
+					CommentStmt *n = makeNode(CommentStmt);
+					n->objtype = OBJECT_PROCEDURE;
+					n->object = (Node *) $4;
+					n->comment = $6;
+					$$ = (Node *) n;
+				}
+			| COMMENT ON ROUTINE function_with_argtypes IS comment_text
+				{
+					CommentStmt *n = makeNode(CommentStmt);
+					n->objtype = OBJECT_ROUTINE;
+					n->object = (Node *) $4;
+					n->comment = $6;
+					$$ = (Node *) n;
+				}
 			| COMMENT ON RULE name ON any_name IS comment_text
 				{
 					CommentStmt *n = makeNode(CommentStmt);
@@ -6542,6 +6591,26 @@ SecLabelStmt:
 					n->label = $9;
 					$$ = (Node *) n;
 				}
+			| SECURITY LABEL opt_provider ON PROCEDURE function_with_argtypes
+			  IS security_label
+				{
+					SecLabelStmt *n = makeNode(SecLabelStmt);
+					n->provider = $3;
+					n->objtype = OBJECT_PROCEDURE;
+					n->object = (Node *) $6;
+					n->label = $8;
+					$$ = (Node *) n;
+				}
+			| SECURITY LABEL opt_provider ON ROUTINE function_with_argtypes
+			  IS security_label
+				{
+					SecLabelStmt *n = makeNode(SecLabelStmt);
+					n->provider = $3;
+					n->objtype = OBJECT_ROUTINE;
+					n->object = (Node *) $6;
+					n->label = $8;
+					$$ = (Node *) n;
+				}
 		;
 
 opt_provider:	FOR NonReservedWord_or_Sconst	{ $$ = $2; }
@@ -6905,6 +6974,22 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| PROCEDURE function_with_argtypes_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = ACL_OBJECT_PROCEDURE;
+					n->objs = $2;
+					$$ = n;
+				}
+			| ROUTINE function_with_argtypes_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = ACL_OBJECT_ROUTINE;
+					n->objs = $2;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -6985,6 +7070,22 @@ privilege_target:
 					n->objs = $5;
 					$$ = n;
 				}
+			| ALL PROCEDURES IN_P SCHEMA name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_ALL_IN_SCHEMA;
+					n->objtype = ACL_OBJECT_PROCEDURE;
+					n->objs = $5;
+					$$ = n;
+				}
+			| ALL ROUTINES IN_P SCHEMA name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_ALL_IN_SCHEMA;
+					n->objtype = ACL_OBJECT_ROUTINE;
+					n->objs = $5;
+					$$ = n;
+				}
 		;
 
 
@@ -7341,6 +7442,18 @@ CreateFunctionStmt:
 					n->withClause = $7;
 					$$ = (Node *)n;
 				}
+			| CREATE opt_or_replace PROCEDURE func_name func_args_with_defaults
+			  createfunc_opt_list
+				{
+					CreateFunctionStmt *n = makeNode(CreateFunctionStmt);
+					n->replace = $2;
+					n->funcname = $4;
+					n->parameters = $5;
+					n->returnType = NULL;
+					n->is_procedure = true;
+					n->options = $6;
+					$$ = (Node *)n;
+				}
 		;
 
 opt_or_replace:
@@ -7758,7 +7871,7 @@ table_func_column_list:
 		;
 
 /*****************************************************************************
- * ALTER FUNCTION
+ * ALTER FUNCTION / ALTER PROCEDURE / ALTER ROUTINE
  *
  * RENAME and OWNER subcommands are already provided by the generic
  * ALTER infrastructure, here we just specify alterations that can
@@ -7769,6 +7882,23 @@ AlterFunctionStmt:
 			ALTER FUNCTION function_with_argtypes alterfunc_opt_list opt_restrict
 				{
 					AlterFunctionStmt *n = makeNode(AlterFunctionStmt);
+					n->objtype = OBJECT_FUNCTION;
+					n->func = $3;
+					n->actions = $4;
+					$$ = (Node *) n;
+				}
+			| ALTER PROCEDURE function_with_argtypes alterfunc_opt_list opt_restrict
+				{
+					AlterFunctionStmt *n = makeNode(AlterFunctionStmt);
+					n->objtype = OBJECT_PROCEDURE;
+					n->func = $3;
+					n->actions = $4;
+					$$ = (Node *) n;
+				}
+			| ALTER ROUTINE function_with_argtypes alterfunc_opt_list opt_restrict
+				{
+					AlterFunctionStmt *n = makeNode(AlterFunctionStmt);
+					n->objtype = OBJECT_ROUTINE;
 					n->func = $3;
 					n->actions = $4;
 					$$ = (Node *) n;
@@ -7793,6 +7923,8 @@ opt_restrict:
  *		QUERY:
  *
  *		DROP FUNCTION funcname (arg1, arg2, ...) [ RESTRICT | CASCADE ]
+ *		DROP PROCEDURE procname (arg1, arg2, ...) [ RESTRICT | CASCADE ]
+ *		DROP ROUTINE routname (arg1, arg2, ...) [ RESTRICT | CASCADE ]
  *		DROP AGGREGATE aggname (arg1, ...) [ RESTRICT | CASCADE ]
  *		DROP OPERATOR opname (leftoperand_typ, rightoperand_typ) [ RESTRICT | CASCADE ]
  *
@@ -7819,6 +7951,46 @@ RemoveFuncStmt:
 					n->concurrent = false;
 					$$ = (Node *)n;
 				}
+			| DROP PROCEDURE function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_PROCEDURE;
+					n->objects = $3;
+					n->behavior = $4;
+					n->missing_ok = false;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
+			| DROP PROCEDURE IF_P EXISTS function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_PROCEDURE;
+					n->objects = $5;
+					n->behavior = $6;
+					n->missing_ok = true;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
+			| DROP ROUTINE function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_ROUTINE;
+					n->objects = $3;
+					n->behavior = $4;
+					n->missing_ok = false;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
+			| DROP ROUTINE IF_P EXISTS function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_ROUTINE;
+					n->objects = $5;
+					n->behavior = $6;
+					n->missing_ok = true;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
 		;
 
 RemoveAggrStmt:
@@ -8276,6 +8448,15 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = true;
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -8285,6 +8466,15 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ROUTINE function_with_argtypes RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER SCHEMA name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -8664,6 +8854,22 @@ AlterObjectDependsStmt:
 					n->extname = makeString($7);
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes DEPENDS ON EXTENSION name
+				{
+					AlterObjectDependsStmt *n = makeNode(AlterObjectDependsStmt);
+					n->objectType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->extname = makeString($7);
+					$$ = (Node *)n;
+				}
+			| ALTER ROUTINE function_with_argtypes DEPENDS ON EXTENSION name
+				{
+					AlterObjectDependsStmt *n = makeNode(AlterObjectDependsStmt);
+					n->objectType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->extname = makeString($7);
+					$$ = (Node *)n;
+				}
 			| ALTER TRIGGER name ON qualified_name DEPENDS ON EXTENSION name
 				{
 					AlterObjectDependsStmt *n = makeNode(AlterObjectDependsStmt);
@@ -8779,6 +8985,24 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER ROUTINE function_with_argtypes SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER TABLE relation_expr SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -9054,6 +9278,22 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $9;
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
+			| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
 			| ALTER SCHEMA name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -14617,6 +14857,7 @@ unreserved_keyword:
 			| BEGIN_P
 			| BY
 			| CACHE
+			| CALL
 			| CALLED
 			| CASCADE
 			| CASCADED
@@ -14776,6 +15017,7 @@ unreserved_keyword:
 			| PRIVILEGES
 			| PROCEDURAL
 			| PROCEDURE
+			| PROCEDURES
 			| PROGRAM
 			| PUBLICATION
 			| QUOTE
@@ -14802,6 +15044,8 @@ unreserved_keyword:
 			| ROLE
 			| ROLLBACK
 			| ROLLUP
+			| ROUTINE
+			| ROUTINES
 			| ROWS
 			| RULE
 			| SAVEPOINT
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 64111f315e..4c4f4cdc3d 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -508,6 +508,14 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 
 			break;
 
+		case EXPR_KIND_CALL:
+			if (isAgg)
+				err = _("aggregate functions are not allowed in CALL arguments");
+			else
+				err = _("grouping operations are not allowed in CALL arguments");
+
+			break;
+
 			/*
 			 * There is intentionally no default: case here, so that the
 			 * compiler will warn if we add a new ParseExprKind without
@@ -883,6 +891,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_PARTITION_EXPRESSION:
 			err = _("window functions are not allowed in partition key expression");
 			break;
+		case EXPR_KIND_CALL:
+			err = _("window functions are not allowed in CALL arguments");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 1aaa5244e6..951582a24f 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -480,6 +480,7 @@ transformIndirection(ParseState *pstate, A_Indirection *ind)
 										  list_make1(result),
 										  last_srf,
 										  NULL,
+										  false,
 										  location);
 			if (newresult == NULL)
 				unknown_attribute(pstate, result, strVal(n), location);
@@ -629,6 +630,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 											 list_make1(node),
 											 pstate->p_last_srf,
 											 NULL,
+											 false,
 											 cref->location);
 				}
 				break;
@@ -676,6 +678,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 											 list_make1(node),
 											 pstate->p_last_srf,
 											 NULL,
+											 false,
 											 cref->location);
 				}
 				break;
@@ -736,6 +739,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 											 list_make1(node),
 											 pstate->p_last_srf,
 											 NULL,
+											 false,
 											 cref->location);
 				}
 				break;
@@ -1477,6 +1481,7 @@ transformFuncCall(ParseState *pstate, FuncCall *fn)
 							 targs,
 							 last_srf,
 							 fn,
+							 false,
 							 fn->location);
 }
 
@@ -1812,6 +1817,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
+		case EXPR_KIND_CALL:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -3462,6 +3468,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "WHEN";
 		case EXPR_KIND_PARTITION_EXPRESSION:
 			return "PARTITION BY";
+		case EXPR_KIND_CALL:
+			return "CALL";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index fc0d6bc2f2..500769b23c 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -71,7 +71,7 @@ static Node *ParseComplexProjection(ParseState *pstate, char *funcname,
  */
 Node *
 ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
-				  Node *last_srf, FuncCall *fn, int location)
+				  Node *last_srf, FuncCall *fn, bool proc_call, int location)
 {
 	bool		is_column = (fn == NULL);
 	List	   *agg_order = (fn ? fn->agg_order : NIL);
@@ -263,7 +263,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 						   actual_arg_types[0], rettype, -1,
 						   COERCION_EXPLICIT, COERCE_EXPLICIT_CALL, location);
 	}
-	else if (fdresult == FUNCDETAIL_NORMAL)
+	else if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
 	{
 		/*
 		 * Normal function found; was there anything indicating it must be an
@@ -306,6 +306,26 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 					 errmsg("OVER specified, but %s is not a window function nor an aggregate function",
 							NameListToString(funcname)),
 					 parser_errposition(pstate, location)));
+
+		if (fdresult == FUNCDETAIL_NORMAL && proc_call)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_FUNCTION),
+					 errmsg("%s is not a procedure",
+							func_signature_string(funcname, nargs,
+												  argnames,
+												  actual_arg_types)),
+					 errhint("To call a function, use SELECT."),
+					 parser_errposition(pstate, location)));
+
+		if (fdresult == FUNCDETAIL_PROCEDURE && !proc_call)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_FUNCTION),
+					 errmsg("%s is a procedure",
+							func_signature_string(funcname, nargs,
+												  argnames,
+												  actual_arg_types)),
+					 errhint("To call a procedure, use CALL."),
+					 parser_errposition(pstate, location)));
 	}
 	else if (fdresult == FUNCDETAIL_AGGREGATE)
 	{
@@ -635,7 +655,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 		check_srf_call_placement(pstate, last_srf, location);
 
 	/* build the appropriate output structure */
-	if (fdresult == FUNCDETAIL_NORMAL)
+	if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
 	{
 		FuncExpr   *funcexpr = makeNode(FuncExpr);
 
@@ -1589,6 +1609,8 @@ func_get_detail(List *funcname,
 			result = FUNCDETAIL_AGGREGATE;
 		else if (pform->proiswindow)
 			result = FUNCDETAIL_WINDOWFUNC;
+		else if (pform->prorettype == InvalidOid)
+			result = FUNCDETAIL_PROCEDURE;
 		else
 			result = FUNCDETAIL_NORMAL;
 		ReleaseSysCache(ftup);
@@ -1984,16 +2006,28 @@ LookupFuncName(List *funcname, int nargs, const Oid *argtypes, bool noError)
 
 /*
  * LookupFuncWithArgs
- *		Like LookupFuncName, but the argument types are specified by a
- *		ObjectWithArgs node.
+ *
+ * Like LookupFuncName, but the argument types are specified by a
+ * ObjectWithArgs node.  Also, this function can check whether the result is a
+ * function, procedure, or aggregate, based on the objtype argument.  Pass
+ * OBJECT_ROUTINE to accept any of them.
+ *
+ * For historical reasons, we also accept aggregates when looking for a
+ * function.
  */
 Oid
-LookupFuncWithArgs(ObjectWithArgs *func, bool noError)
+LookupFuncWithArgs(ObjectType objtype, ObjectWithArgs *func, bool noError)
 {
 	Oid			argoids[FUNC_MAX_ARGS];
 	int			argcount;
 	int			i;
 	ListCell   *args_item;
+	Oid			oid;
+
+	Assert(objtype == OBJECT_AGGREGATE ||
+		   objtype == OBJECT_FUNCTION ||
+		   objtype == OBJECT_PROCEDURE ||
+		   objtype == OBJECT_ROUTINE);
 
 	argcount = list_length(func->objargs);
 	if (argcount > FUNC_MAX_ARGS)
@@ -2013,90 +2047,100 @@ LookupFuncWithArgs(ObjectWithArgs *func, bool noError)
 		args_item = lnext(args_item);
 	}
 
-	return LookupFuncName(func->objname, func->args_unspecified ? -1 : argcount, argoids, noError);
-}
-
-/*
- * LookupAggWithArgs
- *		Find an aggregate function from a given ObjectWithArgs node.
- *
- * This is almost like LookupFuncWithArgs, but the error messages refer
- * to aggregates rather than plain functions, and we verify that the found
- * function really is an aggregate.
- */
-Oid
-LookupAggWithArgs(ObjectWithArgs *agg, bool noError)
-{
-	Oid			argoids[FUNC_MAX_ARGS];
-	int			argcount;
-	int			i;
-	ListCell   *lc;
-	Oid			oid;
-	HeapTuple	ftup;
-	Form_pg_proc pform;
-
-	argcount = list_length(agg->objargs);
-	if (argcount > FUNC_MAX_ARGS)
-		ereport(ERROR,
-				(errcode(ERRCODE_TOO_MANY_ARGUMENTS),
-				 errmsg_plural("functions cannot have more than %d argument",
-							   "functions cannot have more than %d arguments",
-							   FUNC_MAX_ARGS,
-							   FUNC_MAX_ARGS)));
+	/*
+	 * When looking for a function or routine, we pass noError through to
+	 * LookupFuncName and let it make any error messages.  Otherwise, we make
+	 * our own errors for the aggregate and procedure cases.
+	 */
+	oid = LookupFuncName(func->objname, func->args_unspecified ? -1 : argcount, argoids,
+						 (objtype == OBJECT_FUNCTION || objtype == OBJECT_ROUTINE) ? noError : true);
 
-	i = 0;
-	foreach(lc, agg->objargs)
+	if (objtype == OBJECT_FUNCTION)
 	{
-		TypeName   *t = (TypeName *) lfirst(lc);
-
-		argoids[i] = LookupTypeNameOid(NULL, t, noError);
-		i++;
+		/* Make sure it's a function, not a procedure */
+		if (oid && get_func_rettype(oid) == InvalidOid)
+		{
+			if (noError)
+				return InvalidOid;
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("%s is not a function",
+							func_signature_string(func->objname, argcount,
+												  NIL, argoids))));
+		}
 	}
-
-	oid = LookupFuncName(agg->objname, argcount, argoids, true);
-
-	if (!OidIsValid(oid))
+	else if (objtype == OBJECT_PROCEDURE)
 	{
-		if (noError)
-			return InvalidOid;
-		if (argcount == 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_UNDEFINED_FUNCTION),
-					 errmsg("aggregate %s(*) does not exist",
-							NameListToString(agg->objname))));
-		else
+		if (!OidIsValid(oid))
+		{
+			if (noError)
+				return InvalidOid;
+			else if (func->args_unspecified)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("could not find a procedure named \"%s\"",
+								NameListToString(func->objname))));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("procedure %s does not exist",
+								func_signature_string(func->objname, argcount,
+													  NIL, argoids))));
+		}
+
+		/* Make sure it's a procedure */
+		if (get_func_rettype(oid) != InvalidOid)
+		{
+			if (noError)
+				return InvalidOid;
 			ereport(ERROR,
-					(errcode(ERRCODE_UNDEFINED_FUNCTION),
-					 errmsg("aggregate %s does not exist",
-							func_signature_string(agg->objname, argcount,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("%s is not a procedure",
+							func_signature_string(func->objname, argcount,
 												  NIL, argoids))));
+		}
 	}
-
-	/* Make sure it's an aggregate */
-	ftup = SearchSysCache1(PROCOID, ObjectIdGetDatum(oid));
-	if (!HeapTupleIsValid(ftup))	/* should not happen */
-		elog(ERROR, "cache lookup failed for function %u", oid);
-	pform = (Form_pg_proc) GETSTRUCT(ftup);
-
-	if (!pform->proisagg)
+	else if (objtype == OBJECT_AGGREGATE)
 	{
-		ReleaseSysCache(ftup);
-		if (noError)
-			return InvalidOid;
-		/* we do not use the (*) notation for functions... */
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("function %s is not an aggregate",
-						func_signature_string(agg->objname, argcount,
-											  NIL, argoids))));
-	}
+		if (!OidIsValid(oid))
+		{
+			if (noError)
+				return InvalidOid;
+			else if (func->args_unspecified)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("could not find a aggregate named \"%s\"",
+								NameListToString(func->objname))));
+			else if (argcount == 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("aggregate %s(*) does not exist",
+								NameListToString(func->objname))));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("aggregate %s does not exist",
+								func_signature_string(func->objname, argcount,
+													  NIL, argoids))));
+		}
 
-	ReleaseSysCache(ftup);
+		/* Make sure it's an aggregate */
+		if (!get_func_isagg(oid))
+		{
+			if (noError)
+				return InvalidOid;
+			/* we do not use the (*) notation for functions... */
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("function %s is not an aggregate",
+							func_signature_string(func->objname, argcount,
+												  NIL, argoids))));
+		}
+	}
 
 	return oid;
 }
 
-
 /*
  * check_srf_call_placement
  *		Verify that a set-returning function is called in a valid place,
@@ -2236,6 +2280,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_PARTITION_EXPRESSION:
 			err = _("set-returning functions are not allowed in partition key expressions");
 			break;
+		case EXPR_KIND_CALL:
+			err = _("set-returning functions are not allowed in CALL arguments");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 82a707af7b..4da1f8f643 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -657,6 +657,10 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			}
 			break;
 
+		case T_CallStmt:
+			ExecuteCallStmt(pstate, castNode(CallStmt, parsetree));
+			break;
+
 		case T_ClusterStmt:
 			/* we choose to allow this during "read only" transactions */
 			PreventCommandDuringRecovery("CLUSTER");
@@ -1957,9 +1961,15 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_POLICY:
 			tag = "ALTER POLICY";
 			break;
+		case OBJECT_PROCEDURE:
+			tag = "ALTER PROCEDURE";
+			break;
 		case OBJECT_ROLE:
 			tag = "ALTER ROLE";
 			break;
+		case OBJECT_ROUTINE:
+			tag = "ALTER ROUTINE";
+			break;
 		case OBJECT_RULE:
 			tag = "ALTER RULE";
 			break;
@@ -2261,6 +2271,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_FUNCTION:
 					tag = "DROP FUNCTION";
 					break;
+				case OBJECT_PROCEDURE:
+					tag = "DROP PROCEDURE";
+					break;
+				case OBJECT_ROUTINE:
+					tag = "DROP ROUTINE";
+					break;
 				case OBJECT_AGGREGATE:
 					tag = "DROP AGGREGATE";
 					break;
@@ -2359,7 +2375,20 @@ CreateCommandTag(Node *parsetree)
 			break;
 
 		case T_AlterFunctionStmt:
-			tag = "ALTER FUNCTION";
+			switch (((AlterFunctionStmt *) parsetree)->objtype)
+			{
+				case OBJECT_FUNCTION:
+					tag = "ALTER FUNCTION";
+					break;
+				case OBJECT_PROCEDURE:
+					tag = "ALTER PROCEDURE";
+					break;
+				case OBJECT_ROUTINE:
+					tag = "ALTER ROUTINE";
+					break;
+				default:
+					tag = "???";
+			}
 			break;
 
 		case T_GrantStmt:
@@ -2438,7 +2467,10 @@ CreateCommandTag(Node *parsetree)
 			break;
 
 		case T_CreateFunctionStmt:
-			tag = "CREATE FUNCTION";
+			if (((CreateFunctionStmt *) parsetree)->is_procedure)
+				tag = "CREATE PROCEDURE";
+			else
+				tag = "CREATE FUNCTION";
 			break;
 
 		case T_IndexStmt:
@@ -2493,6 +2525,10 @@ CreateCommandTag(Node *parsetree)
 			tag = "LOAD";
 			break;
 
+		case T_CallStmt:
+			tag = "CALL";
+			break;
+
 		case T_ClusterStmt:
 			tag = "CLUSTER";
 			break;
@@ -3116,6 +3152,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_ALL;
 			break;
 
+		case T_CallStmt:
+			lev = LOGSTMT_ALL;
+			break;
+
 		case T_ClusterStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index b1e70a0d19..ca5d117c02 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2674,6 +2674,12 @@ pg_get_function_result(PG_FUNCTION_ARGS)
 	if (!HeapTupleIsValid(proctup))
 		PG_RETURN_NULL();
 
+	if (((Form_pg_proc) GETSTRUCT(proctup))->prorettype == InvalidOid)
+	{
+		ReleaseSysCache(proctup);
+		PG_RETURN_NULL();
+	}
+
 	initStringInfo(&buf);
 
 	print_function_rettype(&buf, proctup);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 48961e31aa..c4e2d33c8d 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1614,6 +1614,25 @@ func_parallel(Oid funcid)
 	return result;
 }
 
+/*
+ * get_func_isagg
+ *	   Given procedure id, return the function's proisagg field.
+ */
+bool
+get_func_isagg(Oid funcid)
+{
+	HeapTuple	tp;
+	bool		result;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+
+	result = ((Form_pg_proc) GETSTRUCT(tp))->proisagg;
+	ReleaseSysCache(tp);
+	return result;
+}
+
 /*
  * get_func_leakproof
  *	   Given procedure id, return the function's leakproof field.
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index e4c95feb63..c0ae4c4e1a 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -33,7 +33,7 @@ static void AddAcl(PQExpBuffer aclbuf, const char *keyword,
  *	name: the object name, in the form to use in the commands (already quoted)
  *	subname: the sub-object name, if any (already quoted); NULL if none
  *	type: the object type (as seen in GRANT command: must be one of
- *		TABLE, SEQUENCE, FUNCTION, LANGUAGE, SCHEMA, DATABASE, TABLESPACE,
+ *		TABLE, SEQUENCE, FUNCTION, PROCEDURE, LANGUAGE, SCHEMA, DATABASE, TABLESPACE,
  *		FOREIGN DATA WRAPPER, SERVER, or LARGE OBJECT)
  *	acls: the ACL string fetched from the database
  *	racls: the ACL string of any initial-but-now-revoked privileges
@@ -524,6 +524,9 @@ do { \
 	else if (strcmp(type, "FUNCTION") == 0 ||
 			 strcmp(type, "FUNCTIONS") == 0)
 		CONVERT_PRIV('X', "EXECUTE");
+	else if (strcmp(type, "PROCEDURE") == 0 ||
+			 strcmp(type, "PROCEDURES") == 0)
+		CONVERT_PRIV('X', "EXECUTE");
 	else if (strcmp(type, "LANGUAGE") == 0)
 		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "SCHEMA") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index ec2fa8b9b9..41741aefbc 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2889,7 +2889,8 @@ _tocEntryRequired(TocEntry *te, teSection curSection, RestoreOptions *ropt)
 			if (ropt->indexNames.head != NULL && (!(simple_string_list_member(&ropt->indexNames, te->tag))))
 				return 0;
 		}
-		else if (strcmp(te->desc, "FUNCTION") == 0)
+		else if (strcmp(te->desc, "FUNCTION") == 0 ||
+				 strcmp(te->desc, "PROCEDURE") == 0)
 		{
 			if (!ropt->selFunction)
 				return 0;
@@ -3388,7 +3389,8 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te, ArchiveHandle *AH)
 		strcmp(type, "FUNCTION") == 0 ||
 		strcmp(type, "OPERATOR") == 0 ||
 		strcmp(type, "OPERATOR CLASS") == 0 ||
-		strcmp(type, "OPERATOR FAMILY") == 0)
+		strcmp(type, "OPERATOR FAMILY") == 0 ||
+		strcmp(type, "PROCEDURE") == 0)
 	{
 		/* Chop "DROP " off the front and make a modifiable copy */
 		char	   *first = pg_strdup(te->dropStmt + 5);
@@ -3560,6 +3562,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 			strcmp(te->desc, "OPERATOR") == 0 ||
 			strcmp(te->desc, "OPERATOR CLASS") == 0 ||
 			strcmp(te->desc, "OPERATOR FAMILY") == 0 ||
+			strcmp(te->desc, "PROCEDURE") == 0 ||
 			strcmp(te->desc, "PROCEDURAL LANGUAGE") == 0 ||
 			strcmp(te->desc, "SCHEMA") == 0 ||
 			strcmp(te->desc, "EVENT TRIGGER") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8733426e8a..cbf3f1656d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11341,6 +11341,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 	char	   *funcargs;
 	char	   *funciargs;
 	char	   *funcresult;
+	bool		is_procedure;
 	char	   *proallargtypes;
 	char	   *proargmodes;
 	char	   *proargnames;
@@ -11362,6 +11363,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 	char	  **argnames = NULL;
 	char	  **configitems = NULL;
 	int			nconfigitems = 0;
+	const char *keyword;
 	int			i;
 
 	/* Skip if not to be dumped */
@@ -11505,7 +11507,11 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 	{
 		funcargs = PQgetvalue(res, 0, PQfnumber(res, "funcargs"));
 		funciargs = PQgetvalue(res, 0, PQfnumber(res, "funciargs"));
-		funcresult = PQgetvalue(res, 0, PQfnumber(res, "funcresult"));
+		is_procedure = PQgetisnull(res, 0, PQfnumber(res, "funcresult"));
+		if (is_procedure)
+			funcresult = NULL;
+		else
+			funcresult = PQgetvalue(res, 0, PQfnumber(res, "funcresult"));
 		proallargtypes = proargmodes = proargnames = NULL;
 	}
 	else
@@ -11514,6 +11520,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 		proargmodes = PQgetvalue(res, 0, PQfnumber(res, "proargmodes"));
 		proargnames = PQgetvalue(res, 0, PQfnumber(res, "proargnames"));
 		funcargs = funciargs = funcresult = NULL;
+		is_procedure = false;
 	}
 	if (PQfnumber(res, "protrftypes") != -1)
 		protrftypes = PQgetvalue(res, 0, PQfnumber(res, "protrftypes"));
@@ -11645,22 +11652,29 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 
 	funcsig_tag = format_function_signature(fout, finfo, false);
 
+	keyword = is_procedure ? "PROCEDURE" : "FUNCTION";
+
 	/*
 	 * DROP must be fully qualified in case same name appears in pg_catalog
 	 */
-	appendPQExpBuffer(delqry, "DROP FUNCTION %s.%s;\n",
+	appendPQExpBuffer(delqry, "DROP %s %s.%s;\n",
+					  keyword,
 					  fmtId(finfo->dobj.namespace->dobj.name),
 					  funcsig);
 
-	appendPQExpBuffer(q, "CREATE FUNCTION %s ", funcfullsig ? funcfullsig :
+	appendPQExpBuffer(q, "CREATE %s %s",
+					  keyword,
+					  funcfullsig ? funcfullsig :
 					  funcsig);
-	if (funcresult)
-		appendPQExpBuffer(q, "RETURNS %s", funcresult);
+	if (is_procedure)
+		;
+	else if (funcresult)
+		appendPQExpBuffer(q, " RETURNS %s", funcresult);
 	else
 	{
 		rettypename = getFormattedTypeName(fout, finfo->prorettype,
 										   zeroAsOpaque);
-		appendPQExpBuffer(q, "RETURNS %s%s",
+		appendPQExpBuffer(q, " RETURNS %s%s",
 						  (proretset[0] == 't') ? "SETOF " : "",
 						  rettypename);
 		free(rettypename);
@@ -11767,7 +11781,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 
 	appendPQExpBuffer(q, "\n    %s;\n", asPart->data);
 
-	appendPQExpBuffer(labelq, "FUNCTION %s", funcsig);
+	appendPQExpBuffer(labelq, "%s %s", keyword, funcsig);
 
 	if (dopt->binary_upgrade)
 		binary_upgrade_extension_member(q, &finfo->dobj, labelq->data);
@@ -11778,7 +11792,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 					 finfo->dobj.namespace->dobj.name,
 					 NULL,
 					 finfo->rolname, false,
-					 "FUNCTION", SECTION_PRE_DATA,
+					 keyword, SECTION_PRE_DATA,
 					 q->data, delqry->data, NULL,
 					 NULL, 0,
 					 NULL, NULL);
@@ -11795,7 +11809,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 					 finfo->dobj.catId, 0, finfo->dobj.dumpId);
 
 	if (finfo->dobj.dump & DUMP_COMPONENT_ACL)
-		dumpACL(fout, finfo->dobj.catId, finfo->dobj.dumpId, "FUNCTION",
+		dumpACL(fout, finfo->dobj.catId, finfo->dobj.dumpId, keyword,
 				funcsig, NULL, funcsig_tag,
 				finfo->dobj.namespace->dobj.name,
 				finfo->rolname, finfo->proacl, finfo->rproacl,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index fa3b56a426..7cf9bdadb2 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3654,6 +3654,44 @@
 			section_data             => 1,
 			section_post_data        => 1, }, },
 
+	'CREATE PROCEDURE dump_test.ptest1' => {
+		all_runs     => 1,
+		create_order => 41,
+		create_sql   => 'CREATE PROCEDURE dump_test.ptest1(a int)
+					   LANGUAGE SQL AS $$ INSERT INTO dump_test.test_table (col1) VALUES (a) $$;',
+		regexp => qr/^
+			\QCREATE PROCEDURE ptest1(a integer)\E
+			\n\s+\QLANGUAGE sql\E
+			\n\s+AS\ \$\$\Q INSERT INTO dump_test.test_table (col1) VALUES (a) \E\$\$;
+			/xm,
+		like => {
+			binary_upgrade          => 1,
+			clean                   => 1,
+			clean_if_exists         => 1,
+			createdb                => 1,
+			defaults                => 1,
+			exclude_test_table      => 1,
+			exclude_test_table_data => 1,
+			no_blobs                => 1,
+			no_privs                => 1,
+			no_owner                => 1,
+			only_dump_test_schema   => 1,
+			pg_dumpall_dbprivs      => 1,
+			schema_only             => 1,
+			section_pre_data        => 1,
+			test_schema_plus_blobs  => 1,
+			with_oids               => 1, },
+		unlike => {
+			column_inserts           => 1,
+			data_only                => 1,
+			exclude_dump_test_schema => 1,
+			only_dump_test_table     => 1,
+			pg_dumpall_globals       => 1,
+			pg_dumpall_globals_clean => 1,
+			role                     => 1,
+			section_data             => 1,
+			section_post_data        => 1, }, },
+
 	'CREATE TYPE dump_test.int42 populated' => {
 		all_runs     => 1,
 		create_order => 42,
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 638275ca2f..7116d67694 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -353,6 +353,7 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 						  " CASE\n"
 						  "  WHEN p.proisagg THEN '%s'\n"
 						  "  WHEN p.proiswindow THEN '%s'\n"
+						  "  WHEN p.prorettype = 0 THEN '%s'\n"
 						  "  WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN '%s'\n"
 						  "  ELSE '%s'\n"
 						  " END as \"%s\"",
@@ -361,8 +362,9 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* translator: "agg" is short for "aggregate" */
 						  gettext_noop("agg"),
 						  gettext_noop("window"),
+						  gettext_noop("proc"),
 						  gettext_noop("trigger"),
-						  gettext_noop("normal"),
+						  gettext_noop("func"),
 						  gettext_noop("Type"));
 	else if (pset.sversion >= 80100)
 		appendPQExpBuffer(&buf,
@@ -407,7 +409,7 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* translator: "agg" is short for "aggregate" */
 						  gettext_noop("agg"),
 						  gettext_noop("trigger"),
-						  gettext_noop("normal"),
+						  gettext_noop("func"),
 						  gettext_noop("Type"));
 	else
 		appendPQExpBuffer(&buf,
@@ -424,7 +426,7 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* translator: "agg" is short for "aggregate" */
 						  gettext_noop("agg"),
 						  gettext_noop("trigger"),
-						  gettext_noop("normal"),
+						  gettext_noop("func"),
 						  gettext_noop("Type"));
 
 	if (verbose)
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index a09c49d6cf..b4f74d0a08 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -397,7 +397,7 @@ static const SchemaQuery Query_for_list_of_functions = {
 	/* catname */
 	"pg_catalog.pg_proc p",
 	/* selcondition */
-	NULL,
+	"p.prorettype <> 0",
 	/* viscondition */
 	"pg_catalog.pg_function_is_visible(p.oid)",
 	/* namespace */
@@ -423,6 +423,36 @@ static const SchemaQuery Query_for_list_of_indexes = {
 	NULL
 };
 
+static const SchemaQuery Query_for_list_of_procedures = {
+	/* catname */
+	"pg_catalog.pg_proc p",
+	/* selcondition */
+	"p.prorettype = 0",
+	/* viscondition */
+	"pg_catalog.pg_function_is_visible(p.oid)",
+	/* namespace */
+	"p.pronamespace",
+	/* result */
+	"pg_catalog.quote_ident(p.proname)",
+	/* qualresult */
+	NULL
+};
+
+static const SchemaQuery Query_for_list_of_routines = {
+	/* catname */
+	"pg_catalog.pg_proc p",
+	/* selcondition */
+	NULL,
+	/* viscondition */
+	"pg_catalog.pg_function_is_visible(p.oid)",
+	/* namespace */
+	"p.pronamespace",
+	/* result */
+	"pg_catalog.quote_ident(p.proname)",
+	/* qualresult */
+	NULL
+};
+
 static const SchemaQuery Query_for_list_of_sequences = {
 	/* catname */
 	"pg_catalog.pg_class c",
@@ -1032,8 +1062,10 @@ static const pgsql_thing_t words_after_create[] = {
 	{"OWNED", NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},	/* for DROP OWNED BY ... */
 	{"PARSER", Query_for_list_of_ts_parsers, NULL, THING_NO_SHOW},
 	{"POLICY", NULL, NULL},
+	{"PROCEDURE", NULL, &Query_for_list_of_procedures},
 	{"PUBLICATION", Query_for_list_of_publications},
 	{"ROLE", Query_for_list_of_roles},
+	{"ROUTINE", NULL, &Query_for_list_of_routines, THING_NO_CREATE},
 	{"RULE", "SELECT pg_catalog.quote_ident(rulename) FROM pg_catalog.pg_rules WHERE substring(pg_catalog.quote_ident(rulename),1,%d)='%s'"},
 	{"SCHEMA", Query_for_list_of_schemas},
 	{"SEQUENCE", NULL, &Query_for_list_of_sequences},
@@ -1407,7 +1439,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* Known command-starting keywords. */
 	static const char *const sql_commands[] = {
-		"ABORT", "ALTER", "ANALYZE", "BEGIN", "CHECKPOINT", "CLOSE", "CLUSTER",
+		"ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER",
 		"COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE",
 		"DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN",
 		"FETCH", "GRANT", "IMPORT", "INSERT", "LISTEN", "LOAD", "LOCK",
@@ -1520,11 +1552,11 @@ psql_completion(const char *text, int start, int end)
 	/* ALTER TABLE,INDEX,MATERIALIZED VIEW ALL IN TABLESPACE xxx OWNED BY xxx */
 	else if (TailMatches7("ALL", "IN", "TABLESPACE", MatchAny, "OWNED", "BY", MatchAny))
 		COMPLETE_WITH_CONST("SET TABLESPACE");
-	/* ALTER AGGREGATE,FUNCTION <name> */
-	else if (Matches3("ALTER", "AGGREGATE|FUNCTION", MatchAny))
+	/* ALTER AGGREGATE,FUNCTION,PROCEDURE,ROUTINE <name> */
+	else if (Matches3("ALTER", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny))
 		COMPLETE_WITH_CONST("(");
-	/* ALTER AGGREGATE,FUNCTION <name> (...) */
-	else if (Matches4("ALTER", "AGGREGATE|FUNCTION", MatchAny, MatchAny))
+	/* ALTER AGGREGATE,FUNCTION,PROCEDURE,ROUTINE <name> (...) */
+	else if (Matches4("ALTER", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny))
 	{
 		if (ends_with(prev_wd, ')'))
 			COMPLETE_WITH_LIST3("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2145,6 +2177,11 @@ psql_completion(const char *text, int start, int end)
 /* ROLLBACK */
 	else if (Matches1("ROLLBACK"))
 		COMPLETE_WITH_LIST4("WORK", "TRANSACTION", "TO SAVEPOINT", "PREPARED");
+/* CALL */
+	else if (Matches1("CALL"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+	else if (Matches2("CALL", MatchAny))
+		COMPLETE_WITH_CONST("(");
 /* CLUSTER */
 	else if (Matches1("CLUSTER"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, "UNION SELECT 'VERBOSE'");
@@ -2176,6 +2213,7 @@ psql_completion(const char *text, int start, int end)
 			"SERVER", "INDEX", "LANGUAGE", "POLICY", "PUBLICATION", "RULE",
 			"SCHEMA", "SEQUENCE", "STATISTICS", "SUBSCRIPTION",
 			"TABLE", "TYPE", "VIEW", "MATERIALIZED VIEW", "COLUMN", "AGGREGATE", "FUNCTION",
+			"PROCEDURE", "ROUTINE",
 			"OPERATOR", "TRIGGER", "CONSTRAINT", "DOMAIN", "LARGE OBJECT",
 		"TABLESPACE", "TEXT SEARCH", "ROLE", NULL};
 
@@ -2685,7 +2723,7 @@ psql_completion(const char *text, int start, int end)
 					  "COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW",
 					  MatchAny) ||
 			 Matches4("DROP", "ACCESS", "METHOD", MatchAny) ||
-			 (Matches4("DROP", "AGGREGATE|FUNCTION", MatchAny, MatchAny) &&
+			 (Matches4("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
 			 Matches4("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches5("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
@@ -2694,9 +2732,9 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH_LIST2("CASCADE", "RESTRICT");
 
 	/* help completing some of the variants */
-	else if (Matches3("DROP", "AGGREGATE|FUNCTION", MatchAny))
+	else if (Matches3("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny))
 		COMPLETE_WITH_CONST("(");
-	else if (Matches4("DROP", "AGGREGATE|FUNCTION", MatchAny, "("))
+	else if (Matches4("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, "("))
 		COMPLETE_WITH_FUNCTION_ARG(prev2_wd);
 	else if (Matches2("DROP", "FOREIGN"))
 		COMPLETE_WITH_LIST2("DATA WRAPPER", "TABLE");
@@ -2893,10 +2931,12 @@ psql_completion(const char *text, int start, int end)
 		 * objects supported.
 		 */
 		if (HeadMatches3("ALTER", "DEFAULT", "PRIVILEGES"))
-			COMPLETE_WITH_LIST5("TABLES", "SEQUENCES", "FUNCTIONS", "TYPES", "SCHEMAS");
+			COMPLETE_WITH_LIST7("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS");
 		else
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvmf,
 									   " UNION SELECT 'ALL FUNCTIONS IN SCHEMA'"
+									   " UNION SELECT 'ALL PROCEDURES IN SCHEMA'"
+									   " UNION SELECT 'ALL ROUTINES IN SCHEMA'"
 									   " UNION SELECT 'ALL SEQUENCES IN SCHEMA'"
 									   " UNION SELECT 'ALL TABLES IN SCHEMA'"
 									   " UNION SELECT 'DATABASE'"
@@ -2906,6 +2946,8 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'FUNCTION'"
 									   " UNION SELECT 'LANGUAGE'"
 									   " UNION SELECT 'LARGE OBJECT'"
+									   " UNION SELECT 'PROCEDURE'"
+									   " UNION SELECT 'ROUTINE'"
 									   " UNION SELECT 'SCHEMA'"
 									   " UNION SELECT 'SEQUENCE'"
 									   " UNION SELECT 'TABLE'"
@@ -2913,7 +2955,10 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'TYPE'");
 	}
 	else if (TailMatches4("GRANT|REVOKE", MatchAny, "ON", "ALL"))
-		COMPLETE_WITH_LIST3("FUNCTIONS IN SCHEMA", "SEQUENCES IN SCHEMA",
+		COMPLETE_WITH_LIST5("FUNCTIONS IN SCHEMA",
+							"PROCEDURES IN SCHEMA",
+							"ROUTINES IN SCHEMA",
+							"SEQUENCES IN SCHEMA",
 							"TABLES IN SCHEMA");
 	else if (TailMatches4("GRANT|REVOKE", MatchAny, "ON", "FOREIGN"))
 		COMPLETE_WITH_LIST2("DATA WRAPPER", "SERVER");
@@ -2934,6 +2979,10 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
 		else if (TailMatches1("LANGUAGE"))
 			COMPLETE_WITH_QUERY(Query_for_list_of_languages);
+		else if (TailMatches1("PROCEDURE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+		else if (TailMatches1("ROUTINE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
 		else if (TailMatches1("SCHEMA"))
 			COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
 		else if (TailMatches1("SEQUENCE"))
@@ -3163,7 +3212,7 @@ psql_completion(const char *text, int start, int end)
 		static const char *const list_SECURITY_LABEL[] =
 		{"TABLE", "COLUMN", "AGGREGATE", "DATABASE", "DOMAIN",
 			"EVENT TRIGGER", "FOREIGN TABLE", "FUNCTION", "LARGE OBJECT",
-			"MATERIALIZED VIEW", "LANGUAGE", "PUBLICATION", "ROLE", "SCHEMA",
+			"MATERIALIZED VIEW", "LANGUAGE", "PUBLICATION", "PROCEDURE", "ROLE", "ROUTINE", "SCHEMA",
 		"SEQUENCE", "SUBSCRIPTION", "TABLESPACE", "TYPE", "VIEW", NULL};
 
 		COMPLETE_WITH_LIST(list_SECURITY_LABEL);
@@ -3233,8 +3282,8 @@ psql_completion(const char *text, int start, int end)
 	/* Complete SET <var> with "TO" */
 	else if (Matches2("SET", MatchAny))
 		COMPLETE_WITH_CONST("TO");
-	/* Complete ALTER DATABASE|FUNCTION|ROLE|USER ... SET <name> */
-	else if (HeadMatches2("ALTER", "DATABASE|FUNCTION|ROLE|USER") &&
+	/* Complete ALTER DATABASE|FUNCTION||PROCEDURE|ROLE|ROUTINE|USER ... SET <name> */
+	else if (HeadMatches2("ALTER", "DATABASE|FUNCTION|PROCEDURE|ROLE|ROUTINE|USER") &&
 			 TailMatches2("SET", MatchAny))
 		COMPLETE_WITH_LIST2("FROM CURRENT", "TO");
 	/* Suggest possible variable values */
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index f7bb4a54f7..13d5925b18 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -59,12 +59,13 @@ extern void DropTransformById(Oid transformOid);
 extern void IsThereFunctionInNamespace(const char *proname, int pronargs,
 						   oidvector *proargtypes, Oid nspOid);
 extern void ExecuteDoStmt(DoStmt *stmt);
+extern void ExecuteCallStmt(ParseState *pstate, CallStmt *stmt);
 extern Oid	get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok);
 extern Oid	get_transform_oid(Oid type_id, Oid lang_id, bool missing_ok);
 extern void interpret_function_parameter_list(ParseState *pstate,
 								  List *parameters,
 								  Oid languageOid,
-								  bool is_aggregate,
+								  ObjectType objtype,
 								  oidvector **parameterTypes,
 								  ArrayType **allParameterTypes,
 								  ArrayType **parameterModes,
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index ffeeb4919b..43ee88bd39 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -413,6 +413,7 @@ typedef enum NodeTag
 	T_DropSubscriptionStmt,
 	T_CreateStatsStmt,
 	T_AlterCollationStmt,
+	T_CallStmt,
 
 	/*
 	 * TAGS FOR PARSE TREE NODES (parsenodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 732e5d6788..b721240577 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -355,6 +355,7 @@ typedef struct FuncCall
 	bool		agg_distinct;	/* arguments were labeled DISTINCT */
 	bool		func_variadic;	/* last argument was labeled VARIADIC */
 	struct WindowDef *over;		/* OVER clause, if any */
+	bool		proc_call;		/* CALL statement */
 	int			location;		/* token location, or -1 if unknown */
 } FuncCall;
 
@@ -1636,9 +1637,11 @@ typedef enum ObjectType
 	OBJECT_OPERATOR,
 	OBJECT_OPFAMILY,
 	OBJECT_POLICY,
+	OBJECT_PROCEDURE,
 	OBJECT_PUBLICATION,
 	OBJECT_PUBLICATION_REL,
 	OBJECT_ROLE,
+	OBJECT_ROUTINE,
 	OBJECT_RULE,
 	OBJECT_SCHEMA,
 	OBJECT_SEQUENCE,
@@ -1849,6 +1852,8 @@ typedef enum GrantObjectType
 	ACL_OBJECT_LANGUAGE,		/* procedural language */
 	ACL_OBJECT_LARGEOBJECT,		/* largeobject */
 	ACL_OBJECT_NAMESPACE,		/* namespace */
+	ACL_OBJECT_PROCEDURE,		/* procedure */
+	ACL_OBJECT_ROUTINE,			/* routine */
 	ACL_OBJECT_TABLESPACE,		/* tablespace */
 	ACL_OBJECT_TYPE				/* type */
 } GrantObjectType;
@@ -2742,6 +2747,7 @@ typedef struct CreateFunctionStmt
 	List	   *funcname;		/* qualified name of function to create */
 	List	   *parameters;		/* a list of FunctionParameter */
 	TypeName   *returnType;		/* the return type */
+	bool		is_procedure;
 	List	   *options;		/* a list of DefElem */
 	List	   *withClause;		/* a list of DefElem */
 } CreateFunctionStmt;
@@ -2768,6 +2774,7 @@ typedef struct FunctionParameter
 typedef struct AlterFunctionStmt
 {
 	NodeTag		type;
+	ObjectType	objtype;
 	ObjectWithArgs *func;		/* name and args of function */
 	List	   *actions;		/* list of DefElem */
 } AlterFunctionStmt;
@@ -2792,6 +2799,16 @@ typedef struct InlineCodeBlock
 	bool		langIsTrusted;	/* trusted property of the language */
 } InlineCodeBlock;
 
+/* ----------------------
+ *		CALL statement
+ * ----------------------
+ */
+typedef struct CallStmt
+{
+	NodeTag		type;
+	FuncCall   *funccall;
+} CallStmt;
+
 /* ----------------------
  *		Alter Object Rename Statement
  * ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f50e45e886..a932400058 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -63,6 +63,7 @@ PG_KEYWORD("boolean", BOOLEAN_P, COL_NAME_KEYWORD)
 PG_KEYWORD("both", BOTH, RESERVED_KEYWORD)
 PG_KEYWORD("by", BY, UNRESERVED_KEYWORD)
 PG_KEYWORD("cache", CACHE, UNRESERVED_KEYWORD)
+PG_KEYWORD("call", CALL, UNRESERVED_KEYWORD)
 PG_KEYWORD("called", CALLED, UNRESERVED_KEYWORD)
 PG_KEYWORD("cascade", CASCADE, UNRESERVED_KEYWORD)
 PG_KEYWORD("cascaded", CASCADED, UNRESERVED_KEYWORD)
@@ -310,6 +311,7 @@ PG_KEYWORD("prior", PRIOR, UNRESERVED_KEYWORD)
 PG_KEYWORD("privileges", PRIVILEGES, UNRESERVED_KEYWORD)
 PG_KEYWORD("procedural", PROCEDURAL, UNRESERVED_KEYWORD)
 PG_KEYWORD("procedure", PROCEDURE, UNRESERVED_KEYWORD)
+PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD)
 PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD)
 PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD)
 PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD)
@@ -340,6 +342,8 @@ PG_KEYWORD("right", RIGHT, TYPE_FUNC_NAME_KEYWORD)
 PG_KEYWORD("role", ROLE, UNRESERVED_KEYWORD)
 PG_KEYWORD("rollback", ROLLBACK, UNRESERVED_KEYWORD)
 PG_KEYWORD("rollup", ROLLUP, UNRESERVED_KEYWORD)
+PG_KEYWORD("routine", ROUTINE, UNRESERVED_KEYWORD)
+PG_KEYWORD("routines", ROUTINES, UNRESERVED_KEYWORD)
 PG_KEYWORD("row", ROW, COL_NAME_KEYWORD)
 PG_KEYWORD("rows", ROWS, UNRESERVED_KEYWORD)
 PG_KEYWORD("rule", RULE, UNRESERVED_KEYWORD)
diff --git a/src/include/parser/parse_func.h b/src/include/parser/parse_func.h
index b4b6084b1b..fccccd21ed 100644
--- a/src/include/parser/parse_func.h
+++ b/src/include/parser/parse_func.h
@@ -24,6 +24,7 @@ typedef enum
 	FUNCDETAIL_NOTFOUND,		/* no matching function */
 	FUNCDETAIL_MULTIPLE,		/* too many matching functions */
 	FUNCDETAIL_NORMAL,			/* found a matching regular function */
+	FUNCDETAIL_PROCEDURE,		/* found a matching procedure */
 	FUNCDETAIL_AGGREGATE,		/* found a matching aggregate function */
 	FUNCDETAIL_WINDOWFUNC,		/* found a matching window function */
 	FUNCDETAIL_COERCION			/* it's a type coercion request */
@@ -31,7 +32,8 @@ typedef enum
 
 
 extern Node *ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
-				  Node *last_srf, FuncCall *fn, int location);
+							   Node *last_srf, FuncCall *fn, bool proc_call,
+							   int location);
 
 extern FuncDetailCode func_get_detail(List *funcname,
 				List *fargs, List *fargnames,
@@ -62,10 +64,8 @@ extern const char *func_signature_string(List *funcname, int nargs,
 
 extern Oid LookupFuncName(List *funcname, int nargs, const Oid *argtypes,
 			   bool noError);
-extern Oid LookupFuncWithArgs(ObjectWithArgs *func,
+extern Oid LookupFuncWithArgs(ObjectType objtype, ObjectWithArgs *func,
 				   bool noError);
-extern Oid LookupAggWithArgs(ObjectWithArgs *agg,
-				  bool noError);
 
 extern void check_srf_call_placement(ParseState *pstate, Node *last_srf,
 						 int location);
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 68930c1f4a..0fc69761a6 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -67,7 +67,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_EXECUTE_PARAMETER,	/* parameter value in EXECUTE */
 	EXPR_KIND_TRIGGER_WHEN,		/* WHEN condition in CREATE TRIGGER */
 	EXPR_KIND_POLICY,			/* USING or WITH CHECK expr in policy */
-	EXPR_KIND_PARTITION_EXPRESSION	/* PARTITION BY expression */
+	EXPR_KIND_PARTITION_EXPRESSION,	/* PARTITION BY expression */
+	EXPR_KIND_CALL				/* CALL argument */
 } ParseExprKind;
 
 
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 07208b56ce..b316cc594c 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -118,6 +118,7 @@ extern bool get_func_retset(Oid funcid);
 extern bool func_strict(Oid funcid);
 extern char func_volatile(Oid funcid);
 extern char func_parallel(Oid funcid);
+extern bool get_func_isagg(Oid funcid);
 extern bool get_func_leakproof(Oid funcid);
 extern float4 get_func_cost(Oid funcid);
 extern float4 get_func_rows(Oid funcid);
diff --git a/src/interfaces/ecpg/preproc/ecpg.tokens b/src/interfaces/ecpg/preproc/ecpg.tokens
index 68ba925efe..1d613af02f 100644
--- a/src/interfaces/ecpg/preproc/ecpg.tokens
+++ b/src/interfaces/ecpg/preproc/ecpg.tokens
@@ -2,7 +2,7 @@
 
 /* special embedded SQL tokens */
 %token  SQL_ALLOCATE SQL_AUTOCOMMIT SQL_BOOL SQL_BREAK
-                SQL_CALL SQL_CARDINALITY SQL_CONNECT
+                SQL_CARDINALITY SQL_CONNECT
                 SQL_COUNT
                 SQL_DATETIME_INTERVAL_CODE
                 SQL_DATETIME_INTERVAL_PRECISION SQL_DESCRIBE
diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer
index f60a62099d..19dc781885 100644
--- a/src/interfaces/ecpg/preproc/ecpg.trailer
+++ b/src/interfaces/ecpg/preproc/ecpg.trailer
@@ -1460,13 +1460,13 @@ action : CONTINUE_P
 			$<action>$.command = NULL;
 			$<action>$.str = mm_strdup("continue");
 		}
-		| SQL_CALL name '(' c_args ')'
+		| CALL name '(' c_args ')'
 		{
 			$<action>$.code = W_DO;
 			$<action>$.command = cat_str(4, $2, mm_strdup("("), $4, mm_strdup(")"));
 			$<action>$.str = cat2_str(mm_strdup("call"), mm_strdup($<action>$.command));
 		}
-		| SQL_CALL name
+		| CALL name
 		{
 			$<action>$.code = W_DO;
 			$<action>$.command = cat2_str($2, mm_strdup("()"));
@@ -1482,7 +1482,6 @@ ECPGKeywords: ECPGKeywords_vanames	{ $$ = $1; }
 		;
 
 ECPGKeywords_vanames:  SQL_BREAK		{ $$ = mm_strdup("break"); }
-		| SQL_CALL						{ $$ = mm_strdup("call"); }
 		| SQL_CARDINALITY				{ $$ = mm_strdup("cardinality"); }
 		| SQL_COUNT						{ $$ = mm_strdup("count"); }
 		| SQL_DATETIME_INTERVAL_CODE	{ $$ = mm_strdup("datetime_interval_code"); }
diff --git a/src/interfaces/ecpg/preproc/ecpg_keywords.c b/src/interfaces/ecpg/preproc/ecpg_keywords.c
index 3b52b8f3a2..848b2d4849 100644
--- a/src/interfaces/ecpg/preproc/ecpg_keywords.c
+++ b/src/interfaces/ecpg/preproc/ecpg_keywords.c
@@ -33,7 +33,6 @@ static const ScanKeyword ECPGScanKeywords[] = {
 	{"autocommit", SQL_AUTOCOMMIT, 0},
 	{"bool", SQL_BOOL, 0},
 	{"break", SQL_BREAK, 0},
-	{"call", SQL_CALL, 0},
 	{"cardinality", SQL_CARDINALITY, 0},
 	{"connect", SQL_CONNECT, 0},
 	{"count", SQL_COUNT, 0},
diff --git a/src/pl/plperl/GNUmakefile b/src/pl/plperl/GNUmakefile
index 91d1296b21..b829027d05 100644
--- a/src/pl/plperl/GNUmakefile
+++ b/src/pl/plperl/GNUmakefile
@@ -55,7 +55,7 @@ endif # win32
 SHLIB_LINK = $(perl_embed_ldflags)
 
 REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=plperl  --load-extension=plperlu
-REGRESS = plperl plperl_lc plperl_trigger plperl_shared plperl_elog plperl_util plperl_init plperlu plperl_array
+REGRESS = plperl plperl_lc plperl_trigger plperl_shared plperl_elog plperl_util plperl_init plperlu plperl_array plperl_call
 # if Perl can support two interpreters in one backend,
 # test plperl-and-plperlu cases
 ifneq ($(PERL),)
diff --git a/src/pl/plperl/expected/plperl_call.out b/src/pl/plperl/expected/plperl_call.out
new file mode 100644
index 0000000000..4bccfcb7c8
--- /dev/null
+++ b/src/pl/plperl/expected/plperl_call.out
@@ -0,0 +1,29 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE plperl
+AS $$
+undef;
+$$;
+CALL test_proc1();
+CREATE PROCEDURE test_proc2()
+LANGUAGE plperl
+AS $$
+return 5
+$$;
+CALL test_proc2();
+CREATE TABLE test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plperl
+AS $$
+spi_exec_query("INSERT INTO test1 VALUES ($_[0])");
+$$;
+CALL test_proc3(55);
+SELECT * FROM test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE test1;
diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index ca0d1bccf8..edd99f8013 100644
--- a/src/pl/plperl/plperl.c
+++ b/src/pl/plperl/plperl.c
@@ -1915,7 +1915,7 @@ plperl_inline_handler(PG_FUNCTION_ARGS)
 	desc.fn_retistuple = false;
 	desc.fn_retisset = false;
 	desc.fn_retisarray = false;
-	desc.result_oid = VOIDOID;
+	desc.result_oid = InvalidOid;
 	desc.nargs = 0;
 	desc.reference = NULL;
 
@@ -2481,7 +2481,7 @@ plperl_func_handler(PG_FUNCTION_ARGS)
 		}
 		retval = (Datum) 0;
 	}
-	else
+	else if (prodesc->result_oid)
 	{
 		retval = plperl_sv_to_datum(perlret,
 									prodesc->result_oid,
@@ -2826,7 +2826,7 @@ compile_plperl_function(Oid fn_oid, bool is_trigger, bool is_event_trigger)
 		 * Get the required information for input conversion of the
 		 * return value.
 		 ************************************************************/
-		if (!is_trigger && !is_event_trigger)
+		if (!is_trigger && !is_event_trigger && procStruct->prorettype)
 		{
 			Oid			rettype = procStruct->prorettype;
 
@@ -3343,7 +3343,7 @@ plperl_return_next_internal(SV *sv)
 
 		tuplestore_puttuple(current_call_data->tuple_store, tuple);
 	}
-	else
+	else if (prodesc->result_oid)
 	{
 		Datum		ret[1];
 		bool		isNull[1];
diff --git a/src/pl/plperl/sql/plperl_call.sql b/src/pl/plperl/sql/plperl_call.sql
new file mode 100644
index 0000000000..bd2b63b418
--- /dev/null
+++ b/src/pl/plperl/sql/plperl_call.sql
@@ -0,0 +1,36 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE plperl
+AS $$
+undef;
+$$;
+
+CALL test_proc1();
+
+
+CREATE PROCEDURE test_proc2()
+LANGUAGE plperl
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plperl
+AS $$
+spi_exec_query("INSERT INTO test1 VALUES ($_[0])");
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index 9931ee038f..00b89ea056 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -275,7 +275,6 @@ do_compile(FunctionCallInfo fcinfo,
 	bool		isnull;
 	char	   *proc_source;
 	HeapTuple	typeTup;
-	Form_pg_type typeStruct;
 	PLpgSQL_variable *var;
 	PLpgSQL_rec *rec;
 	int			i;
@@ -531,53 +530,58 @@ do_compile(FunctionCallInfo fcinfo,
 			/*
 			 * Lookup the function's return type
 			 */
-			typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rettypeid));
-			if (!HeapTupleIsValid(typeTup))
-				elog(ERROR, "cache lookup failed for type %u", rettypeid);
-			typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
-
-			/* Disallow pseudotype result, except VOID or RECORD */
-			/* (note we already replaced polymorphic types) */
-			if (typeStruct->typtype == TYPTYPE_PSEUDO)
+			if (rettypeid)
 			{
-				if (rettypeid == VOIDOID ||
-					rettypeid == RECORDOID)
-					 /* okay */ ;
-				else if (rettypeid == TRIGGEROID || rettypeid == EVTTRIGGEROID)
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("trigger functions can only be called as triggers")));
-				else
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("PL/pgSQL functions cannot return type %s",
-									format_type_be(rettypeid))));
-			}
+				Form_pg_type typeStruct;
 
-			if (typeStruct->typrelid != InvalidOid ||
-				rettypeid == RECORDOID)
-				function->fn_retistuple = true;
-			else
-			{
-				function->fn_retbyval = typeStruct->typbyval;
-				function->fn_rettyplen = typeStruct->typlen;
+				typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rettypeid));
+				if (!HeapTupleIsValid(typeTup))
+					elog(ERROR, "cache lookup failed for type %u", rettypeid);
+				typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
 
-				/*
-				 * install $0 reference, but only for polymorphic return
-				 * types, and not when the return is specified through an
-				 * output parameter.
-				 */
-				if (IsPolymorphicType(procStruct->prorettype) &&
-					num_out_args == 0)
+				/* Disallow pseudotype result, except VOID or RECORD */
+				/* (note we already replaced polymorphic types) */
+				if (typeStruct->typtype == TYPTYPE_PSEUDO)
 				{
-					(void) plpgsql_build_variable("$0", 0,
-												  build_datatype(typeTup,
-																 -1,
-																 function->fn_input_collation),
-												  true);
+					if (rettypeid == VOIDOID ||
+						rettypeid == RECORDOID)
+						/* okay */ ;
+					else if (rettypeid == TRIGGEROID || rettypeid == EVTTRIGGEROID)
+						ereport(ERROR,
+								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+								 errmsg("trigger functions can only be called as triggers")));
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+								 errmsg("PL/pgSQL functions cannot return type %s",
+										format_type_be(rettypeid))));
+				}
+
+				if (typeStruct->typrelid != InvalidOid ||
+					rettypeid == RECORDOID)
+					function->fn_retistuple = true;
+				else
+				{
+					function->fn_retbyval = typeStruct->typbyval;
+					function->fn_rettyplen = typeStruct->typlen;
+
+					/*
+					 * install $0 reference, but only for polymorphic return
+					 * types, and not when the return is specified through an
+					 * output parameter.
+					 */
+					if (IsPolymorphicType(procStruct->prorettype) &&
+						num_out_args == 0)
+					{
+						(void) plpgsql_build_variable("$0", 0,
+													  build_datatype(typeTup,
+																	 -1,
+																	 function->fn_input_collation),
+													  true);
+					}
 				}
+				ReleaseSysCache(typeTup);
 			}
-			ReleaseSysCache(typeTup);
 			break;
 
 		case PLPGSQL_DML_TRIGGER:
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index e605ec829e..1069fdd6ad 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -462,7 +462,7 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo,
 	estate.err_text = NULL;
 	estate.err_stmt = (PLpgSQL_stmt *) (func->action);
 	rc = exec_stmt_block(&estate, func->action);
-	if (rc != PLPGSQL_RC_RETURN)
+	if (rc != PLPGSQL_RC_RETURN && func->fn_rettype)
 	{
 		estate.err_stmt = NULL;
 		estate.err_text = NULL;
@@ -509,6 +509,12 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo,
 	}
 	else if (!estate.retisnull)
 	{
+		if (!func->fn_rettype)
+		{
+			ereport(ERROR,
+					(errmsg("cannot return a value from a procedure")));
+		}
+
 		if (estate.retistuple)
 		{
 			/*
diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile
index 7680d49cb6..cc91afebde 100644
--- a/src/pl/plpython/Makefile
+++ b/src/pl/plpython/Makefile
@@ -78,6 +78,7 @@ REGRESS = \
 	plpython_spi \
 	plpython_newline \
 	plpython_void \
+	plpython_call \
 	plpython_params \
 	plpython_setof \
 	plpython_record \
diff --git a/src/pl/plpython/expected/plpython_call.out b/src/pl/plpython/expected/plpython_call.out
new file mode 100644
index 0000000000..58c184e2ea
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_call.out
@@ -0,0 +1,35 @@
+--
+-- Tests for procedures / CALL syntax
+--
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpythonu
+AS $$
+pass
+$$;
+CALL test_proc1();
+-- error: can't return non-None
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpythonu
+AS $$
+return 5
+$$;
+CALL test_proc2();
+ERROR:  PL/Python procedure did not return None
+CONTEXT:  PL/Python function "test_proc2"
+CREATE TABLE test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpythonu
+AS $$
+plpy.execute("INSERT INTO test1 VALUES (%s)" % x)
+$$;
+CALL test_proc3(55);
+SELECT * FROM test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE test1;
diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
index 26f61dd0f3..1b7a9470d4 100644
--- a/src/pl/plpython/plpy_exec.c
+++ b/src/pl/plpython/plpy_exec.c
@@ -197,12 +197,19 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 		error_context_stack = &plerrcontext;
 
 		/*
-		 * If the function is declared to return void, the Python return value
+		 * For a procedure or function declared to return void, the Python return value
 		 * must be None. For void-returning functions, we also treat a None
 		 * return value as a special "void datum" rather than NULL (as is the
 		 * case for non-void-returning functions).
 		 */
-		if (proc->result.out.d.typoid == VOIDOID)
+		if (proc->is_procedure)
+		{
+			if (plrv != Py_None)
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("PL/Python procedure did not return None")));
+		}
+		else if (proc->result.out.d.typoid == VOIDOID)
 		{
 			if (plrv != Py_None)
 				ereport(ERROR,
@@ -715,7 +722,8 @@ plpython_return_error_callback(void *arg)
 {
 	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 
-	if (exec_ctx->curr_proc)
+	if (exec_ctx->curr_proc &&
+		!exec_ctx->curr_proc->is_procedure)
 		errcontext("while creating return value");
 }
 
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index 26acc88b27..7b04a60e90 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -188,6 +188,7 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 		proc->fn_tid = procTup->t_self;
 		proc->fn_readonly = (procStruct->provolatile != PROVOLATILE_VOLATILE);
 		proc->is_setof = procStruct->proretset;
+		proc->is_procedure = (procStruct->prorettype == InvalidOid);
 		PLy_typeinfo_init(&proc->result, proc->mcxt);
 		proc->src = NULL;
 		proc->argnames = NULL;
@@ -207,9 +208,9 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 
 		/*
 		 * get information required for output conversion of the return value,
-		 * but only if this isn't a trigger.
+		 * but only if this isn't a trigger or procedure.
 		 */
-		if (!is_trigger)
+		if (!is_trigger && procStruct->prorettype)
 		{
 			HeapTuple	rvTypeTup;
 			Form_pg_type rvTypeStruct;
diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h
index d05944fc39..0bb1b652b0 100644
--- a/src/pl/plpython/plpy_procedure.h
+++ b/src/pl/plpython/plpy_procedure.h
@@ -30,7 +30,8 @@ typedef struct PLyProcedure
 	TransactionId fn_xmin;
 	ItemPointerData fn_tid;
 	bool		fn_readonly;
-	bool		is_setof;		/* true, if procedure returns result set */
+	bool		is_setof;		/* true, if function returns result set */
+	bool		is_procedure;
 	PLyTypeInfo result;			/* also used to store info for trigger tuple
 								 * type */
 	char	   *src;			/* textual procedure code, after mangling */
diff --git a/src/pl/plpython/sql/plpython_call.sql b/src/pl/plpython/sql/plpython_call.sql
new file mode 100644
index 0000000000..3fb74de5f0
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_call.sql
@@ -0,0 +1,41 @@
+--
+-- Tests for procedures / CALL syntax
+--
+
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpythonu
+AS $$
+pass
+$$;
+
+CALL test_proc1();
+
+
+-- error: can't return non-None
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpythonu
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpythonu
+AS $$
+plpy.execute("INSERT INTO test1 VALUES (%s)" % x)
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/pl/tcl/Makefile b/src/pl/tcl/Makefile
index b8971d3cc8..6a92a9b6aa 100644
--- a/src/pl/tcl/Makefile
+++ b/src/pl/tcl/Makefile
@@ -28,7 +28,7 @@ DATA = pltcl.control pltcl--1.0.sql pltcl--unpackaged--1.0.sql \
        pltclu.control pltclu--1.0.sql pltclu--unpackaged--1.0.sql
 
 REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=pltcl
-REGRESS = pltcl_setup pltcl_queries pltcl_start_proc pltcl_subxact pltcl_unicode
+REGRESS = pltcl_setup pltcl_queries pltcl_call pltcl_start_proc pltcl_subxact pltcl_unicode
 
 # Tcl on win32 ships with import libraries only for Microsoft Visual C++,
 # which are not compatible with mingw gcc. Therefore we need to build a
diff --git a/src/pl/tcl/expected/pltcl_call.out b/src/pl/tcl/expected/pltcl_call.out
new file mode 100644
index 0000000000..7221a37ad0
--- /dev/null
+++ b/src/pl/tcl/expected/pltcl_call.out
@@ -0,0 +1,29 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE pltcl
+AS $$
+unset
+$$;
+CALL test_proc1();
+CREATE PROCEDURE test_proc2()
+LANGUAGE pltcl
+AS $$
+return 5
+$$;
+CALL test_proc2();
+CREATE TABLE test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE pltcl
+AS $$
+spi_exec "INSERT INTO test1 VALUES ($1)"
+$$;
+CALL test_proc3(55);
+SELECT * FROM test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE test1;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index 6d97ddc99b..e0792d93e1 100644
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -146,6 +146,7 @@ typedef struct pltcl_proc_desc
 	Oid			result_typid;	/* OID of fn's result type */
 	FmgrInfo	result_in_func; /* input function for fn's result type */
 	Oid			result_typioparam;	/* param to pass to same */
+	bool		fn_is_procedure;/* true if this is a procedure */
 	bool		fn_retisset;	/* true if function returns a set */
 	bool		fn_retistuple;	/* true if function returns composite */
 	bool		fn_retisdomain; /* true if function returns domain */
@@ -968,7 +969,7 @@ pltcl_func_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
 		retval = (Datum) 0;
 		fcinfo->isnull = true;
 	}
-	else if (fcinfo->isnull)
+	else if (fcinfo->isnull && !prodesc->fn_is_procedure)
 	{
 		retval = InputFunctionCall(&prodesc->result_in_func,
 								   NULL,
@@ -1026,11 +1027,13 @@ pltcl_func_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
 									   call_state);
 		retval = HeapTupleGetDatum(tup);
 	}
-	else
+	else if (!prodesc->fn_is_procedure)
 		retval = InputFunctionCall(&prodesc->result_in_func,
 								   utf_u2e(Tcl_GetStringResult(interp)),
 								   prodesc->result_typioparam,
 								   -1);
+	else
+		retval = 0;
 
 	return retval;
 }
@@ -1506,7 +1509,9 @@ compile_pltcl_function(Oid fn_oid, Oid tgreloid,
 		 * Get the required information for input conversion of the
 		 * return value.
 		 ************************************************************/
-		if (!is_trigger && !is_event_trigger)
+		prodesc->fn_is_procedure = (procStruct->prorettype == InvalidOid);
+
+		if (!is_trigger && !is_event_trigger && procStruct->prorettype)
 		{
 			Oid			rettype = procStruct->prorettype;
 
@@ -2199,7 +2204,7 @@ pltcl_returnnext(ClientData cdata, Tcl_Interp *interp,
 				tuplestore_puttuple(call_state->tuple_store, tuple);
 			}
 		}
-		else
+		else if (!prodesc->fn_is_procedure)
 		{
 			Datum		retval;
 			bool		isNull = false;
diff --git a/src/pl/tcl/sql/pltcl_call.sql b/src/pl/tcl/sql/pltcl_call.sql
new file mode 100644
index 0000000000..ef1f540f50
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_call.sql
@@ -0,0 +1,36 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE pltcl
+AS $$
+unset
+$$;
+
+CALL test_proc1();
+
+
+CREATE PROCEDURE test_proc2()
+LANGUAGE pltcl
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE pltcl
+AS $$
+spi_exec "INSERT INTO test1 VALUES ($1)"
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/test/regress/expected/create_procedure.out b/src/test/regress/expected/create_procedure.out
new file mode 100644
index 0000000000..eeb129d71f
--- /dev/null
+++ b/src/test/regress/expected/create_procedure.out
@@ -0,0 +1,86 @@
+CALL nonexistent();  -- error
+ERROR:  function nonexistent() does not exist
+LINE 1: CALL nonexistent();
+             ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+CALL random();  -- error
+ERROR:  random() is not a procedure
+LINE 1: CALL random();
+             ^
+HINT:  To call a function, use SELECT.
+CREATE FUNCTION testfunc1(a int) RETURNS int LANGUAGE SQL AS $$ SELECT a $$;
+CREATE TABLE cp_test (a int, b text);
+CREATE PROCEDURE ptest1(x text)
+LANGUAGE SQL
+AS $$
+INSERT INTO cp_test VALUES (1, x);
+$$;
+SELECT ptest1('x');  -- error
+ERROR:  ptest1(unknown) is a procedure
+LINE 1: SELECT ptest1('x');
+               ^
+HINT:  To call a procedure, use CALL.
+CALL ptest1('a');  -- ok
+\df ptest1
+                        List of functions
+ Schema |  Name  | Result data type | Argument data types | Type 
+--------+--------+------------------+---------------------+------
+ public | ptest1 |                  | x text              | proc
+(1 row)
+
+SELECT * FROM cp_test ORDER BY a;
+ a | b 
+---+---
+ 1 | a
+(1 row)
+
+CREATE PROCEDURE ptest2()
+LANGUAGE SQL
+AS $$
+SELECT 5;
+$$;
+CALL ptest2();
+-- various error cases
+CREATE PROCEDURE ptestx() LANGUAGE SQL WINDOW AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+ERROR:  procedure cannot have WINDOW attribute
+CREATE PROCEDURE ptestx() LANGUAGE SQL STRICT AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+ERROR:  procedure cannot be declared as strict
+CREATE PROCEDURE ptestx(OUT a int) LANGUAGE SQL AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+ERROR:  procedures cannot have OUT parameters
+ALTER PROCEDURE ptest1(text) STRICT;
+ERROR:  procedure strictness cannot be changed
+ALTER FUNCTION ptest1(text) VOLATILE;  -- error: not a function
+ERROR:  ptest1(text) is not a function
+ALTER PROCEDURE testfunc1(int) VOLATILE;  -- error: not a procedure
+ERROR:  testfunc1(integer) is not a procedure
+ALTER PROCEDURE nonexistent() VOLATILE;
+ERROR:  procedure nonexistent() does not exist
+DROP FUNCTION ptest1(text);  -- error: not a function
+ERROR:  ptest1(text) is not a function
+DROP PROCEDURE testfunc1(int);  -- error: not a procedure
+ERROR:  testfunc1(integer) is not a procedure
+DROP PROCEDURE nonexistent();
+ERROR:  procedure nonexistent() does not exist
+-- privileges
+CREATE USER regress_user1;
+GRANT INSERT ON cp_test TO regress_user1;
+REVOKE EXECUTE ON PROCEDURE ptest1(text) FROM PUBLIC;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- error
+ERROR:  permission denied for function ptest1
+RESET ROLE;
+GRANT EXECUTE ON PROCEDURE ptest1(text) TO regress_user1;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- ok
+RESET ROLE;
+-- ROUTINE syntax
+ALTER ROUTINE testfunc1(int) RENAME TO testfunc1a;
+ALTER ROUTINE testfunc1a RENAME TO testfunc1;
+ALTER ROUTINE ptest1(text) RENAME TO ptest1a;
+ALTER ROUTINE ptest1a RENAME TO ptest1;
+DROP ROUTINE testfunc1(int);
+-- cleanup
+DROP PROCEDURE ptest1;
+DROP PROCEDURE ptest2;
+DROP TABLE cp_test;
+DROP USER regress_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 1fdadbc9ef..bfd9d54c11 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -29,6 +29,7 @@ CREATE DOMAIN addr_nsp.gendomain AS int4 CONSTRAINT domconstr CHECK (value > 0);
 CREATE FUNCTION addr_nsp.trig() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN END; $$;
 CREATE TRIGGER t BEFORE INSERT ON addr_nsp.gentable FOR EACH ROW EXECUTE PROCEDURE addr_nsp.trig();
 CREATE POLICY genpol ON addr_nsp.gentable;
+CREATE PROCEDURE addr_nsp.proc(int4) LANGUAGE SQL AS $$ $$;
 CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
@@ -88,7 +89,7 @@ BEGIN
 		('table'), ('index'), ('sequence'), ('view'),
 		('materialized view'), ('foreign table'),
 		('table column'), ('foreign table column'),
-		('aggregate'), ('function'), ('type'), ('cast'),
+		('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
 		('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
 		('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
 		('text search parser'), ('text search dictionary'),
@@ -171,6 +172,12 @@ WARNING:  error for function,{addr_nsp,zwei},{}: function addr_nsp.zwei() does n
 WARNING:  error for function,{addr_nsp,zwei},{integer}: function addr_nsp.zwei(integer) does not exist
 WARNING:  error for function,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for function,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for procedure,{eins},{}: procedure eins() does not exist
+WARNING:  error for procedure,{eins},{integer}: procedure eins(integer) does not exist
+WARNING:  error for procedure,{addr_nsp,zwei},{}: procedure addr_nsp.zwei() does not exist
+WARNING:  error for procedure,{addr_nsp,zwei},{integer}: procedure addr_nsp.zwei(integer) does not exist
+WARNING:  error for procedure,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for procedure,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for type,{eins},{}: type "eins" does not exist
 WARNING:  error for type,{eins},{integer}: type "eins" does not exist
 WARNING:  error for type,{addr_nsp,zwei},{}: name list length must be exactly 1
@@ -371,6 +378,7 @@ WITH objects (type, name, args) AS (VALUES
 				('foreign table column', '{addr_nsp, genftable, a}', '{}'),
 				('aggregate', '{addr_nsp, genaggr}', '{int4}'),
 				('function', '{pg_catalog, pg_identify_object}', '{pg_catalog.oid, pg_catalog.oid, int4}'),
+				('procedure', '{addr_nsp, proc}', '{int4}'),
 				('type', '{pg_catalog._int4}', '{}'),
 				('type', '{addr_nsp.gendomain}', '{}'),
 				('type', '{addr_nsp.gencomptype}', '{}'),
@@ -431,6 +439,7 @@ SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*,
  type                      | addr_nsp   | gendomain         | addr_nsp.gendomain                                                   | t
  function                  | pg_catalog |                   | pg_catalog.pg_identify_object(pg_catalog.oid,pg_catalog.oid,integer) | t
  aggregate                 | addr_nsp   |                   | addr_nsp.genaggr(integer)                                            | t
+ procedure                 | addr_nsp   |                   | addr_nsp.proc(integer)                                               | t
  sequence                  | addr_nsp   | gentable_a_seq    | addr_nsp.gentable_a_seq                                              | t
  table                     | addr_nsp   | gentable          | addr_nsp.gentable                                                    | t
  table column              | addr_nsp   | gentable          | addr_nsp.gentable.b                                                  | t
@@ -469,7 +478,7 @@ SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*,
  subscription              |            | addr_sub          | addr_sub                                                             | t
  publication               |            | addr_pub          | addr_pub                                                             | t
  publication relation      |            |                   | gentable in publication addr_pub                                     | t
-(46 rows)
+(47 rows)
 
 ---
 --- Cleanup resources
@@ -480,6 +489,6 @@ NOTICE:  drop cascades to 4 other objects
 DROP PUBLICATION addr_pub;
 DROP SUBSCRIPTION addr_sub;
 DROP SCHEMA addr_nsp CASCADE;
-NOTICE:  drop cascades to 12 other objects
+NOTICE:  drop cascades to 13 other objects
 DROP OWNED BY regress_addr_user;
 DROP USER regress_addr_user;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index bb3532676b..d6e5bc3353 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -6040,3 +6040,44 @@ END; $$ LANGUAGE plpgsql;
 ERROR:  "x" is not a scalar variable
 LINE 3:   GET DIAGNOSTICS x = ROW_COUNT;
                           ^
+--
+-- Procedures
+--
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    NULL;
+END;
+$$;
+CALL test_proc1();
+-- error: can't return non-NULL
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN 5;
+END;
+$$;
+CALL test_proc2();
+ERROR:  cannot return a value from a procedure
+CONTEXT:  PL/pgSQL function test_proc2() while casting return value to function's return type
+CREATE TABLE proc_test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    INSERT INTO proc_test1 VALUES (x);
+END;
+$$;
+CALL test_proc3(55);
+SELECT * FROM proc_test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE proc_test1;
diff --git a/src/test/regress/expected/polymorphism.out b/src/test/regress/expected/polymorphism.out
index 91cfb743b6..66e35a6a5c 100644
--- a/src/test/regress/expected/polymorphism.out
+++ b/src/test/regress/expected/polymorphism.out
@@ -915,10 +915,10 @@ select dfunc();
 
 -- verify it lists properly
 \df dfunc
-                                           List of functions
- Schema | Name  | Result data type |                    Argument data types                    |  Type  
---------+-------+------------------+-----------------------------------------------------------+--------
- public | dfunc | integer          | a integer DEFAULT 1, OUT sum integer, b integer DEFAULT 2 | normal
+                                          List of functions
+ Schema | Name  | Result data type |                    Argument data types                    | Type 
+--------+-------+------------------+-----------------------------------------------------------+------
+ public | dfunc | integer          | a integer DEFAULT 1, OUT sum integer, b integer DEFAULT 2 | func
 (1 row)
 
 drop function dfunc(int, int);
@@ -1083,10 +1083,10 @@ $$ select array_upper($1, 1) $$ language sql;
 ERROR:  cannot remove parameter defaults from existing function
 HINT:  Use DROP FUNCTION dfunc(integer[]) first.
 \df dfunc
-                                      List of functions
- Schema | Name  | Result data type |               Argument data types               |  Type  
---------+-------+------------------+-------------------------------------------------+--------
- public | dfunc | integer          | VARIADIC a integer[] DEFAULT ARRAY[]::integer[] | normal
+                                     List of functions
+ Schema | Name  | Result data type |               Argument data types               | Type 
+--------+-------+------------------+-------------------------------------------------+------
+ public | dfunc | integer          | VARIADIC a integer[] DEFAULT ARRAY[]::integer[] | func
 (1 row)
 
 drop function dfunc(a variadic int[]);
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index aa5e6af621..9343aa02c7 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -53,7 +53,7 @@ test: copy copyselect copydml
 # ----------
 # More groups of parallel tests
 # ----------
-test: create_misc create_operator
+test: create_misc create_operator create_procedure
 # These depend on the above two
 test: create_index create_view
 
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 3866314a92..8dd4b7ba0c 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -63,6 +63,7 @@ test: copyselect
 test: copydml
 test: create_misc
 test: create_operator
+test: create_procedure
 test: create_index
 test: create_view
 test: create_aggregate
diff --git a/src/test/regress/sql/create_procedure.sql b/src/test/regress/sql/create_procedure.sql
new file mode 100644
index 0000000000..f09ba2ad30
--- /dev/null
+++ b/src/test/regress/sql/create_procedure.sql
@@ -0,0 +1,79 @@
+CALL nonexistent();  -- error
+CALL random();  -- error
+
+CREATE FUNCTION testfunc1(a int) RETURNS int LANGUAGE SQL AS $$ SELECT a $$;
+
+CREATE TABLE cp_test (a int, b text);
+
+CREATE PROCEDURE ptest1(x text)
+LANGUAGE SQL
+AS $$
+INSERT INTO cp_test VALUES (1, x);
+$$;
+
+SELECT ptest1('x');  -- error
+CALL ptest1('a');  -- ok
+
+\df ptest1
+
+SELECT * FROM cp_test ORDER BY a;
+
+
+CREATE PROCEDURE ptest2()
+LANGUAGE SQL
+AS $$
+SELECT 5;
+$$;
+
+CALL ptest2();
+
+
+-- various error cases
+
+CREATE PROCEDURE ptestx() LANGUAGE SQL WINDOW AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+CREATE PROCEDURE ptestx() LANGUAGE SQL STRICT AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+CREATE PROCEDURE ptestx(OUT a int) LANGUAGE SQL AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+
+ALTER PROCEDURE ptest1(text) STRICT;
+ALTER FUNCTION ptest1(text) VOLATILE;  -- error: not a function
+ALTER PROCEDURE testfunc1(int) VOLATILE;  -- error: not a procedure
+ALTER PROCEDURE nonexistent() VOLATILE;
+
+DROP FUNCTION ptest1(text);  -- error: not a function
+DROP PROCEDURE testfunc1(int);  -- error: not a procedure
+DROP PROCEDURE nonexistent();
+
+
+-- privileges
+
+CREATE USER regress_user1;
+GRANT INSERT ON cp_test TO regress_user1;
+REVOKE EXECUTE ON PROCEDURE ptest1(text) FROM PUBLIC;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- error
+RESET ROLE;
+GRANT EXECUTE ON PROCEDURE ptest1(text) TO regress_user1;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- ok
+RESET ROLE;
+
+
+-- ROUTINE syntax
+
+ALTER ROUTINE testfunc1(int) RENAME TO testfunc1a;
+ALTER ROUTINE testfunc1a RENAME TO testfunc1;
+
+ALTER ROUTINE ptest1(text) RENAME TO ptest1a;
+ALTER ROUTINE ptest1a RENAME TO ptest1;
+
+DROP ROUTINE testfunc1(int);
+
+
+-- cleanup
+
+DROP PROCEDURE ptest1;
+DROP PROCEDURE ptest2;
+
+DROP TABLE cp_test;
+
+DROP USER regress_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 63821b8008..55faa71edf 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -32,6 +32,7 @@ CREATE DOMAIN addr_nsp.gendomain AS int4 CONSTRAINT domconstr CHECK (value > 0);
 CREATE FUNCTION addr_nsp.trig() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN END; $$;
 CREATE TRIGGER t BEFORE INSERT ON addr_nsp.gentable FOR EACH ROW EXECUTE PROCEDURE addr_nsp.trig();
 CREATE POLICY genpol ON addr_nsp.gentable;
+CREATE PROCEDURE addr_nsp.proc(int4) LANGUAGE SQL AS $$ $$;
 CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
@@ -81,7 +82,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 		('table'), ('index'), ('sequence'), ('view'),
 		('materialized view'), ('foreign table'),
 		('table column'), ('foreign table column'),
-		('aggregate'), ('function'), ('type'), ('cast'),
+		('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
 		('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
 		('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
 		('text search parser'), ('text search dictionary'),
@@ -147,6 +148,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 				('foreign table column', '{addr_nsp, genftable, a}', '{}'),
 				('aggregate', '{addr_nsp, genaggr}', '{int4}'),
 				('function', '{pg_catalog, pg_identify_object}', '{pg_catalog.oid, pg_catalog.oid, int4}'),
+				('procedure', '{addr_nsp, proc}', '{int4}'),
 				('type', '{pg_catalog._int4}', '{}'),
 				('type', '{addr_nsp.gendomain}', '{}'),
 				('type', '{addr_nsp.gencomptype}', '{}'),
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index 6620ea6172..1c355132b7 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -4820,3 +4820,52 @@ CREATE FUNCTION fx(x WSlot) RETURNS void AS $$
   GET DIAGNOSTICS x = ROW_COUNT;
   RETURN;
 END; $$ LANGUAGE plpgsql;
+
+
+--
+-- Procedures
+--
+
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    NULL;
+END;
+$$;
+
+CALL test_proc1();
+
+
+-- error: can't return non-NULL
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN 5;
+END;
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE proc_test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    INSERT INTO proc_test1 VALUES (x);
+END;
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM proc_test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE proc_test1;

base-commit: ee4673ac071f8352c41cc673299b7ec695f079ff
-- 
2.14.3

#2Simon Riggs
simon@2ndquadrant.com
In reply to: Peter Eisentraut (#1)
Re: SQL procedures

On 31 October 2017 at 18:23, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

I've been working on SQL procedures. (Some might call them "stored
procedures", but I'm not aware of any procedures that are not stored, so
that's not a term that I'm using here.)

I guess that the DO command might have a variant to allow you to
execute a procedure that isn't stored?

Not suggesting you implement that, just thinking about why/when the
"stored" word would be appropriate.

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

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

#3Tom Lane
tgl@sss.pgh.pa.us
In reply to: Peter Eisentraut (#1)
Re: SQL procedures

Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:

I've been working on SQL procedures.

No comment yet on the big picture here, but ...

The provided procedural languages (an ever more
confusing term) each needed a small touch-up to handle pg_proc entries
with prorettype == 0.

Putting 0 in prorettype seems like a pretty bad idea. Why not use VOIDOID
for the prorettype value? Or if there is some reason why "void" isn't the
right pseudotype, maybe you should invent a new one, analogous to the
"trigger" and "event_trigger" pseudotypes.

regards, tom lane

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

#4Pavel Stehule
pavel.stehule@gmail.com
In reply to: Peter Eisentraut (#1)
Re: SQL procedures

2017-10-31 18:23 GMT+01:00 Peter Eisentraut <
peter.eisentraut@2ndquadrant.com>:

I've been working on SQL procedures. (Some might call them "stored
procedures", but I'm not aware of any procedures that are not stored, so
that's not a term that I'm using here.)

Everything that follows is intended to align with the SQL standard, at
least in spirit.

This first patch does a bunch of preparation work. It adds the
CREATE/ALTER/DROP PROCEDURE commands and the CALL statement to call a
procedure. It also adds ROUTINE syntax which can refer to a function or
procedure. I have extended that to include aggregates. And then there
is a bunch of leg work, such as psql and pg_dump support. The
documentation is a lot of copy-and-paste right now; that can be
revisited sometime. The provided procedural languages (an ever more
confusing term) each needed a small touch-up to handle pg_proc entries
with prorettype == 0.

Right now, there is no support for returning values from procedures via
OUT parameters. That will need some definitional pondering; and see
also below for a possible alternative.

With this, you can write procedures that are somewhat compatible with
DB2, MySQL, and to a lesser extent Oracle.

Separately, I will send patches that implement (the beginnings of) two
separate features on top of this:

- Transaction control in procedure bodies

- Returning multiple result sets

(In various previous discussions on "real stored procedures" or
something like that, most people seemed to have one of these two
features in mind. I think that depends on what other SQL systems one
has worked with previously.)

great. I hope so I can help with testing

Regards

Pavel

Show quoted text

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

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

#5Pavel Stehule
pavel.stehule@gmail.com
In reply to: Peter Eisentraut (#1)
Re: SQL procedures

2017-10-31 18:23 GMT+01:00 Peter Eisentraut <
peter.eisentraut@2ndquadrant.com>:

I've been working on SQL procedures. (Some might call them "stored
procedures", but I'm not aware of any procedures that are not stored, so
that's not a term that I'm using here.)

Everything that follows is intended to align with the SQL standard, at
least in spirit.

This first patch does a bunch of preparation work. It adds the
CREATE/ALTER/DROP PROCEDURE commands and the CALL statement to call a
procedure. It also adds ROUTINE syntax which can refer to a function or
procedure. I have extended that to include aggregates. And then there
is a bunch of leg work, such as psql and pg_dump support. The
documentation is a lot of copy-and-paste right now; that can be
revisited sometime. The provided procedural languages (an ever more
confusing term) each needed a small touch-up to handle pg_proc entries
with prorettype == 0.

Right now, there is no support for returning values from procedures via
OUT parameters. That will need some definitional pondering; and see
also below for a possible alternative.

With this, you can write procedures that are somewhat compatible with
DB2, MySQL, and to a lesser extent Oracle.

Separately, I will send patches that implement (the beginnings of) two
separate features on top of this:

- Transaction control in procedure bodies

- Returning multiple result sets

(In various previous discussions on "real stored procedures" or
something like that, most people seemed to have one of these two
features in mind. I think that depends on what other SQL systems one
has worked with previously.)

Not sure if disabling RETURN is good idea. I can imagine so optional
returning something like int status can be good idea. Cheaper than raising
a exception.

Regards

Pavel

Show quoted text

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

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

#6Simon Riggs
simon@2ndquadrant.com
In reply to: Peter Eisentraut (#1)
Re: SQL procedures

On 31 October 2017 at 17:23, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

I've been working on SQL procedures. (Some might call them "stored
procedures", but I'm not aware of any procedures that are not stored, so
that's not a term that I'm using here.)

Looks good

Everything that follows is intended to align with the SQL standard, at
least in spirit.

+1

This first patch does a bunch of preparation work. It adds the
CREATE/ALTER/DROP PROCEDURE commands and the CALL statement to call a
procedure.

I guess it would be really useful to have a cut-down language to use
as an example, but its probably easier to just wait for PLpgSQL.

You mention PARALLEL SAFE is not used for procedures. Isn't it an
architectural restriction that procedures would not be able to execute
in parallel? (At least this year)

It also adds ROUTINE syntax which can refer to a function or
procedure.

I think we need an explanatory section of the docs, but there doesn't
seem to be one for Functions, so there is no place to add some text
that says the above.

I found it confusing that ALTER and DROP ROUTINE exists but not CREATE
ROUTINE. At very least we should say somewhere "there is no CREATE
ROUTINE", so its absence is clearly intentional. I did wonder whether
we should have it as well, but its just one less thing to review, so
good.

Was surprised that pg_dump didn't use DROP ROUTINE, when appropriate.

I have extended that to include aggregates. And then there
is a bunch of leg work, such as psql and pg_dump support. The
documentation is a lot of copy-and-paste right now; that can be
revisited sometime. The provided procedural languages (an ever more
confusing term) each needed a small touch-up to handle pg_proc entries
with prorettype == 0.

Right now, there is no support for returning values from procedures via
OUT parameters. That will need some definitional pondering; and see
also below for a possible alternative.

With this, you can write procedures that are somewhat compatible with
DB2, MySQL, and to a lesser extent Oracle.

Separately, I will send patches that implement (the beginnings of) two
separate features on top of this:

- Transaction control in procedure bodies

- Returning multiple result sets

Both of those would be good, though my suggested priority would be
transaction control first and then multiple result sets, if we cannot
have both this release.

(In various previous discussions on "real stored procedures" or
something like that, most people seemed to have one of these two
features in mind. I think that depends on what other SQL systems one
has worked with previously.)

Almost all of the meat happens in later patches, so no other review comments.

That seems seems strange in a patch of this size, but its true.
Procedures are just a new type of object with very little interaction
with replication, persistence or optimization.

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

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

#7Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Tom Lane (#3)
Re: SQL procedures

On 10/31/17 14:23, Tom Lane wrote:

Putting 0 in prorettype seems like a pretty bad idea.

It seemed like the natural thing to do, since we use a zero OID to
indicate "nothing" in many other places.

Why not use VOIDOID for the prorettype value?

We need a way to distinguish functions that are callable by SELECT and
procedures that are callable by CALL.

Or if there is some reason why "void" isn't the
right pseudotype, maybe you should invent a new one, analogous to the
"trigger" and "event_trigger" pseudotypes.

I guess that would be doable, but I think it would make things more
complicated without any gain that I can see. In the case of the
pseudotypes you mention, those are the actual types mentioned in the
CREATE FUNCTION command. If we invented a new pseudotype, that would
run the risk of existing code creating nonsensical reverse compilations
like CREATE FUNCTION RETURNS PROCEDURE. Catalog queries using
prorettype == 0 would behave sensibly by default. For example, an inner
or outer join against pg_type would automatically make sense.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

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

#8Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Pavel Stehule (#5)
Re: SQL procedures

On 10/31/17 16:50, Pavel Stehule wrote:

Not sure if disabling RETURN is good idea. I can imagine so optional
returning something like int status can be good idea. Cheaper than
raising a exception.

We could allow a RETURN without argument in PL/pgSQL, if you just want
to exit early. That syntax is currently not available, but it should
not be hard to add.

I don't understand the point about wanting to return an int. How would
you pass that around, since there is no declared return type?

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

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

#9Merlin Moncure
mmoncure@gmail.com
In reply to: Peter Eisentraut (#1)
Re: SQL procedures

On Tue, Oct 31, 2017 at 12:23 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

- Transaction control in procedure bodies

This feature is really key, since it enables via SQL lots of things
that are not possible without external coding, including:
*) very long running processes in a single routine
*) transaction isolation control inside the procedure (currently
client app has to declare this)
*) certain error handling cases that require client side support
*) simple in-database threading
*) simple construction of daemon scripts (yeah, you can use bgworker
for this, but pure sql daemon with a cron heartbeat hook is hard to
beat for simplicity)

I do wonder how transaction control could be added later.

The last time I (lightly) looked at this, I was starting to think that
working transaction control into the SPI interface was the wrong
approach; pl/pgsql would have to adopt a very different set of
behaviors if it was called in a function or a proc. If you restricted
language choice to purely SQL, you could work around this problem; SPI
languages would be totally abstracted from those sets of
considerations and you could always call an arbitrary language
function if you needed to. SQL has no flow control but I'm not too
concerned about that.

merlin

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

#10Pavel Stehule
pavel.stehule@gmail.com
In reply to: Peter Eisentraut (#8)
Re: SQL procedures

2017-11-08 15:23 GMT+01:00 Peter Eisentraut <
peter.eisentraut@2ndquadrant.com>:

On 10/31/17 16:50, Pavel Stehule wrote:

Not sure if disabling RETURN is good idea. I can imagine so optional
returning something like int status can be good idea. Cheaper than
raising a exception.

We could allow a RETURN without argument in PL/pgSQL, if you just want
to exit early. That syntax is currently not available, but it should
not be hard to add.

I don't understand the point about wanting to return an int. How would
you pass that around, since there is no declared return type?

We can create auto session variable STATUS. This variable can be 0 if
procedure was returned without explicit RETURN value. Or it can hold
different value specified by RETURN expr.

This value can be read by GET DIAGNOSTICS xxx = STATUS

or some similar.

Show quoted text

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#11Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#10)
Re: SQL procedures

2017-11-08 15:31 GMT+01:00 Pavel Stehule <pavel.stehule@gmail.com>:

2017-11-08 15:23 GMT+01:00 Peter Eisentraut <peter.eisentraut@2ndquadrant.
com>:

On 10/31/17 16:50, Pavel Stehule wrote:

Not sure if disabling RETURN is good idea. I can imagine so optional
returning something like int status can be good idea. Cheaper than
raising a exception.

We could allow a RETURN without argument in PL/pgSQL, if you just want
to exit early. That syntax is currently not available, but it should
not be hard to add.

I don't understand the point about wanting to return an int. How would
you pass that around, since there is no declared return type?

We can create auto session variable STATUS. This variable can be 0 if
procedure was returned without explicit RETURN value. Or it can hold
different value specified by RETURN expr.

This value can be read by GET DIAGNOSTICS xxx = STATUS

or some similar.

The motivation is allow some mechanism cheaper than our exceptions.

Regards

Pavel

Show quoted text

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#12Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Merlin Moncure (#9)
Re: SQL procedures

On 08.11.2017 17:23, Merlin Moncure wrote:

On Tue, Oct 31, 2017 at 12:23 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

- Transaction control in procedure bodies

This feature is really key, since it enables via SQL lots of things
that are not possible without external coding, including:
*) very long running processes in a single routine
*) transaction isolation control inside the procedure (currently
client app has to declare this)
*) certain error handling cases that require client side support
*) simple in-database threading
*) simple construction of daemon scripts (yeah, you can use bgworker
for this, but pure sql daemon with a cron heartbeat hook is hard to
beat for simplicity)

I do wonder how transaction control could be added later.

The last time I (lightly) looked at this, I was starting to think that
working transaction control into the SPI interface was the wrong
approach; pl/pgsql would have to adopt a very different set of
behaviors if it was called in a function or a proc. If you restricted
language choice to purely SQL, you could work around this problem; SPI
languages would be totally abstracted from those sets of
considerations and you could always call an arbitrary language
function if you needed to. SQL has no flow control but I'm not too
concerned about that.

merlin

I am also very interested in answer on this question: how you are going
to implement transaction control inside procedure?
Right now in PostgresPRO EE supports autonomous transactions. Them are
supported both for SQL and plpgsql/plpython APIs.
Them are implemented by saving/restoring transaction context, so unlike
most of other ATX implementations, in pgpro autonomous
transaction is executed by the same backend. But it is not so easy to
do: in Postgres almost any module have its own static variables which
keeps transaction specific data.
So we have to provide a dozen of suspend/resume functions:
SuspendSnapshot(),  SuspendPredicate(), SuspendStorage(),
SuspendInvalidationInfo(), SuspendPgXact(), PgStatSuspend(),
TriggerSuspend(), SuspendSPI()... and properly handle local cache
invalidation. Patch consists of more than 5 thousand lines.

So my question is whether you are going to implement something similar
or use completely different approach?
In first case it will be good to somehow unite our efforts... For
example we can publish our ATX patch for Postgres 10.
We have not done it yet, because there seems to be no chances to push
this patch to community.

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

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

#13Tom Lane
tgl@sss.pgh.pa.us
In reply to: Peter Eisentraut (#7)
Re: SQL procedures

Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:

On 10/31/17 14:23, Tom Lane wrote:

Why not use VOIDOID for the prorettype value?

We need a way to distinguish functions that are callable by SELECT and
procedures that are callable by CALL.

Do procedures of this ilk belong in pg_proc at all? It seems like a large
fraction of the attributes tracked in pg_proc are senseless for this
purpose. A new catalog might be a better approach.

In any case, I buy none of your arguments that 0 is a better choice than a
new pseudotype.

regards, tom lane

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

#14Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Simon Riggs (#6)
Re: SQL procedures

On 11/6/17 16:27, Simon Riggs wrote:

You mention PARALLEL SAFE is not used for procedures. Isn't it an
architectural restriction that procedures would not be able to execute
in parallel? (At least this year)

I'm not sure what you are referring to here. I don't think the
functionality I'm proposing does anything in parallel or has any
interaction with it.

I think we need an explanatory section of the docs, but there doesn't
seem to be one for Functions, so there is no place to add some text
that says the above.

I found it confusing that ALTER and DROP ROUTINE exists but not CREATE
ROUTINE. At very least we should say somewhere "there is no CREATE
ROUTINE", so its absence is clearly intentional. I did wonder whether
we should have it as well, but its just one less thing to review, so
good.

I'll look for a place to add some documentation around this.

Was surprised that pg_dump didn't use DROP ROUTINE, when appropriate.

It's not clear to me why that would be preferred.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

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

#15Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Pavel Stehule (#11)
Re: SQL procedures

On 11/8/17 09:33, Pavel Stehule wrote:

We can create auto session variable STATUS. This variable can be 0
if procedure was returned without explicit RETURN value. Or it can
hold different value specified by RETURN expr.

This value can be read by GET DIAGNOSTICS xxx = STATUS

or some similar.

The motivation is allow some mechanism cheaper than our exceptions.

I suppose this could be a separately discussed feature. We'd also want
to consider various things that PL/pgSQL pretends to be compatible with.

One of the main motivations for procedures is to do more complex and
expensive things including transaction control. So saving exception
overhead is not really on the priority list there.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

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

#16Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Merlin Moncure (#9)
Re: SQL procedures

On 11/8/17 09:23, Merlin Moncure wrote:

I do wonder how transaction control could be added later.

The last time I (lightly) looked at this, I was starting to think that
working transaction control into the SPI interface was the wrong
approach; pl/pgsql would have to adopt a very different set of
behaviors if it was called in a function or a proc. If you restricted
language choice to purely SQL, you could work around this problem; SPI
languages would be totally abstracted from those sets of
considerations and you could always call an arbitrary language
function if you needed to. SQL has no flow control but I'm not too
concerned about that.

I have already submitted a separate patch that addresses these questions.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

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

#17Merlin Moncure
mmoncure@gmail.com
In reply to: Peter Eisentraut (#16)
Re: SQL procedures

On Wed, Nov 8, 2017 at 9:13 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

I have already submitted a separate patch that addresses these questions.

Maybe I'm obtuse, but I'm not seeing it? In very interested in the
general approach to transaction management; if you've described it in
the patch I'll read it there. Thanks for doing this.

merlin

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

#18Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Merlin Moncure (#17)
Re: SQL procedures

On 11/8/17 11:11, Merlin Moncure wrote:

On Wed, Nov 8, 2017 at 9:13 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

I have already submitted a separate patch that addresses these questions.

Maybe I'm obtuse, but I'm not seeing it? In very interested in the
general approach to transaction management; if you've described it in
the patch I'll read it there. Thanks for doing this.

/messages/by-id/178d3380-0fae-2982-00d6-c43100bc8748@2ndquadrant.com

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

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

#19Merlin Moncure
mmoncure@gmail.com
In reply to: Peter Eisentraut (#18)
Re: SQL procedures

On Wed, Nov 8, 2017 at 11:03 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

On 11/8/17 11:11, Merlin Moncure wrote:

On Wed, Nov 8, 2017 at 9:13 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

I have already submitted a separate patch that addresses these questions.

Maybe I'm obtuse, but I'm not seeing it? In very interested in the
general approach to transaction management; if you've described it in
the patch I'll read it there. Thanks for doing this.

/messages/by-id/178d3380-0fae-2982-00d6-c43100bc8748@2ndquadrant.com

All right, thanks. So,
*) Are you sure you want to go the SPI route? 'sql' language
(non-spi) procedures might be simpler from implementation standpoint
and do not need any language adjustments?

*) Is it possible to jump into SPI without having a snapshot already
set up. For example? If I wanted to set isolation level in a
procedure, would I get impacted by this error?
ERROR: SET TRANSACTION ISOLATION LEVEL must be called before any query

merlin

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

#20Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Merlin Moncure (#19)
Re: [HACKERS] SQL procedures

On 11/8/17 12:15, Merlin Moncure wrote:

On Wed, Nov 8, 2017 at 11:03 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

On 11/8/17 11:11, Merlin Moncure wrote:

On Wed, Nov 8, 2017 at 9:13 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

I have already submitted a separate patch that addresses these questions.

Maybe I'm obtuse, but I'm not seeing it? In very interested in the
general approach to transaction management; if you've described it in
the patch I'll read it there. Thanks for doing this.

/messages/by-id/178d3380-0fae-2982-00d6-c43100bc8748@2ndquadrant.com

All right, thanks. So,

Could we have this discussion in that other thread? This thread is just
about procedures at mostly a syntax level.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#21Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Peter Eisentraut (#14)
2 attachment(s)
Re: [HACKERS] SQL procedures

Here is an updated patch. It's updated for the recent documentation
format changes. I added some more documentation as suggested by reviewers.

I also added more tests about how the various privilege commands (GRANT,
GRANT on ALL, default privileges) would work with the new object type.
I had not looked at that in much detail with the previous version of the
patch, but it seems to work the way I would have wanted without any code
changes, so it's all documentation additions and new tests.

As part of this I have also developed additional tests for how the same
privilege commands apply to aggregates, which didn't appear to be
covered yet, and I was worried that I might have broken it, which it
seems I did not. This is included in the big patch, but I have also
included it here as a separate patch, as it could be committed
independently as additional tests for existing functionality.

It this point, this patch has no more open TODOs or
need-to-think-about-this-later's that I'm aware of.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

v2-0001-SQL-procedures.patchtext/plain; charset=UTF-8; name=v2-0001-SQL-procedures.patch; x-mac-creator=0; x-mac-type=0Download
From b150165e3b177bd9833385036b7ffbaf060ad8e2 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Tue, 14 Nov 2017 10:42:47 -0500
Subject: [PATCH v2] SQL procedures

CREATE/ALTER/DROP PROCEDURE, ALTER/DROP ROUTINE, CALL statement, utility
statement support, support in the built-in PLs, support in pg_dump and
psql
---
 doc/src/sgml/catalogs.sgml                     |   2 +-
 doc/src/sgml/ddl.sgml                          |   2 +-
 doc/src/sgml/ecpg.sgml                         |   4 +-
 doc/src/sgml/information_schema.sgml           |  18 +-
 doc/src/sgml/plpgsql.sgml                      |   2 +-
 doc/src/sgml/ref/allfiles.sgml                 |   6 +
 doc/src/sgml/ref/alter_default_privileges.sgml |   8 +-
 doc/src/sgml/ref/alter_extension.sgml          |  12 +-
 doc/src/sgml/ref/alter_function.sgml           |   2 +
 doc/src/sgml/ref/alter_procedure.sgml          | 303 +++++++++++++++++++++
 doc/src/sgml/ref/alter_routine.sgml            | 102 +++++++
 doc/src/sgml/ref/call.sgml                     |  97 +++++++
 doc/src/sgml/ref/comment.sgml                  |  13 +-
 doc/src/sgml/ref/create_function.sgml          |   8 +-
 doc/src/sgml/ref/create_procedure.sgml         | 363 +++++++++++++++++++++++++
 doc/src/sgml/ref/drop_function.sgml            |   2 +
 doc/src/sgml/ref/drop_procedure.sgml           | 162 +++++++++++
 doc/src/sgml/ref/drop_routine.sgml             |  94 +++++++
 doc/src/sgml/ref/grant.sgml                    |  25 +-
 doc/src/sgml/ref/revoke.sgml                   |   4 +-
 doc/src/sgml/ref/security_label.sgml           |  12 +-
 doc/src/sgml/reference.sgml                    |   6 +
 doc/src/sgml/xfunc.sgml                        |  33 +++
 src/backend/catalog/aclchk.c                   |  60 +++-
 src/backend/catalog/information_schema.sql     |  25 +-
 src/backend/catalog/objectaddress.c            |  19 +-
 src/backend/catalog/pg_proc.c                  |   3 +-
 src/backend/commands/aggregatecmds.c           |   2 +-
 src/backend/commands/alter.c                   |   6 +
 src/backend/commands/dropcmds.c                |  38 ++-
 src/backend/commands/event_trigger.c           |  14 +
 src/backend/commands/functioncmds.c            | 131 ++++++++-
 src/backend/commands/opclasscmds.c             |   4 +-
 src/backend/executor/functions.c               |  13 +-
 src/backend/nodes/copyfuncs.c                  |  15 +
 src/backend/nodes/equalfuncs.c                 |  13 +
 src/backend/optimizer/util/clauses.c           |   1 +
 src/backend/parser/gram.y                      | 255 ++++++++++++++++-
 src/backend/parser/parse_agg.c                 |  11 +
 src/backend/parser/parse_expr.c                |   8 +
 src/backend/parser/parse_func.c                | 201 ++++++++------
 src/backend/tcop/utility.c                     |  44 ++-
 src/backend/utils/adt/ruleutils.c              |   6 +
 src/backend/utils/cache/lsyscache.c            |  19 ++
 src/bin/pg_dump/dumputils.c                    |   5 +-
 src/bin/pg_dump/pg_backup_archiver.c           |   7 +-
 src/bin/pg_dump/pg_dump.c                      |  32 ++-
 src/bin/pg_dump/t/002_pg_dump.pl               |  38 +++
 src/bin/psql/describe.c                        |   8 +-
 src/bin/psql/tab-complete.c                    |  77 +++++-
 src/include/commands/defrem.h                  |   3 +-
 src/include/nodes/nodes.h                      |   1 +
 src/include/nodes/parsenodes.h                 |  17 ++
 src/include/parser/kwlist.h                    |   4 +
 src/include/parser/parse_func.h                |   8 +-
 src/include/parser/parse_node.h                |   3 +-
 src/include/utils/lsyscache.h                  |   1 +
 src/interfaces/ecpg/preproc/ecpg.tokens        |   2 +-
 src/interfaces/ecpg/preproc/ecpg.trailer       |   5 +-
 src/interfaces/ecpg/preproc/ecpg_keywords.c    |   1 -
 src/pl/plperl/GNUmakefile                      |   2 +-
 src/pl/plperl/expected/plperl_call.out         |  29 ++
 src/pl/plperl/plperl.c                         |   8 +-
 src/pl/plperl/sql/plperl_call.sql              |  36 +++
 src/pl/plpgsql/src/pl_comp.c                   |  88 +++---
 src/pl/plpgsql/src/pl_exec.c                   |   8 +-
 src/pl/plpython/Makefile                       |   1 +
 src/pl/plpython/expected/plpython_call.out     |  35 +++
 src/pl/plpython/plpy_exec.c                    |  14 +-
 src/pl/plpython/plpy_procedure.c               |   5 +-
 src/pl/plpython/plpy_procedure.h               |   3 +-
 src/pl/plpython/sql/plpython_call.sql          |  41 +++
 src/pl/tcl/Makefile                            |   2 +-
 src/pl/tcl/expected/pltcl_call.out             |  29 ++
 src/pl/tcl/pltcl.c                             |  13 +-
 src/pl/tcl/sql/pltcl_call.sql                  |  36 +++
 src/test/regress/expected/create_procedure.out |  86 ++++++
 src/test/regress/expected/object_address.out   |  15 +-
 src/test/regress/expected/plpgsql.out          |  41 +++
 src/test/regress/expected/polymorphism.out     |  16 +-
 src/test/regress/expected/privileges.out       | 128 ++++++++-
 src/test/regress/parallel_schedule             |   2 +-
 src/test/regress/serial_schedule               |   1 +
 src/test/regress/sql/create_procedure.sql      |  79 ++++++
 src/test/regress/sql/object_address.sql        |   4 +-
 src/test/regress/sql/plpgsql.sql               |  49 ++++
 src/test/regress/sql/privileges.sql            |  55 +++-
 87 files changed, 2912 insertions(+), 294 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_procedure.sgml
 create mode 100644 doc/src/sgml/ref/alter_routine.sgml
 create mode 100644 doc/src/sgml/ref/call.sgml
 create mode 100644 doc/src/sgml/ref/create_procedure.sgml
 create mode 100644 doc/src/sgml/ref/drop_procedure.sgml
 create mode 100644 doc/src/sgml/ref/drop_routine.sgml
 create mode 100644 src/pl/plperl/expected/plperl_call.out
 create mode 100644 src/pl/plperl/sql/plperl_call.sql
 create mode 100644 src/pl/plpython/expected/plpython_call.out
 create mode 100644 src/pl/plpython/sql/plpython_call.sql
 create mode 100644 src/pl/tcl/expected/pltcl_call.out
 create mode 100644 src/pl/tcl/sql/pltcl_call.sql
 create mode 100644 src/test/regress/expected/create_procedure.out
 create mode 100644 src/test/regress/sql/create_procedure.sql

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ef60a58631..f36930c721 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5241,7 +5241,7 @@ <title><structname>pg_proc</structname> Columns</title>
       <entry><structfield>prorettype</structfield></entry>
       <entry><type>oid</type></entry>
       <entry><literal><link linkend="catalog-pg-type"><structname>pg_type</structname></link>.oid</literal></entry>
-      <entry>Data type of the return value</entry>
+      <entry>Data type of the return value, or null for a procedure</entry>
      </row>
 
      <row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index daba66c187..efc11b2a38 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3947,7 +3947,7 @@ <title>Other Database Objects</title>
 
    <listitem>
     <para>
-     Functions and operators
+     Functions, procedures, and operators
     </para>
    </listitem>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index bc3d080774..f503f2d35f 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4778,7 +4778,9 @@ <title>Setting Callbacks</title>
       <term><literal>DO <replaceable>name</replaceable> (<replaceable>args</replaceable>)</literal></term>
       <listitem>
        <para>
-        Call the specified C functions with the specified arguments.
+        Call the specified C functions with the specified arguments.  (This
+        use is different from the meaning of <literal>CALL</literal>
+        and <literal>DO</literal> in the normal PostgreSQL grammar.)
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 58c54254d7..ab28acc854 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -3972,8 +3972,8 @@ <title><literal>routine_privileges</literal> Columns</title>
   <title><literal>routines</literal></title>
 
   <para>
-   The view <literal>routines</literal> contains all functions in the
-   current database.  Only those functions are shown that the current
+   The view <literal>routines</literal> contains all functions and procedures in the
+   current database.  Only those functions and procedures are shown that the current
    user has access to (by way of being the owner or having some
    privilege).
   </para>
@@ -4037,8 +4037,8 @@ <title><literal>routines</literal> Columns</title>
       <entry><literal>routine_type</literal></entry>
       <entry><type>character_data</type></entry>
       <entry>
-       Always <literal>FUNCTION</literal> (In the future there might
-       be other types of routines.)
+       <literal>FUNCTION</literal> for a
+       function, <literal>PROCEDURE</literal> for a procedure
       </entry>
      </row>
 
@@ -4087,7 +4087,7 @@ <title><literal>routines</literal> Columns</title>
        the view <literal>element_types</literal>), else
        <literal>USER-DEFINED</literal> (in that case, the type is
        identified in <literal>type_udt_name</literal> and associated
-       columns).
+       columns).  Null for a procedure.
       </entry>
      </row>
 
@@ -4180,7 +4180,7 @@ <title><literal>routines</literal> Columns</title>
       <entry><type>sql_identifier</type></entry>
       <entry>
        Name of the database that the return data type of the function
-       is defined in (always the current database)
+       is defined in (always the current database).  Null for a procedure.
       </entry>
      </row>
 
@@ -4189,7 +4189,7 @@ <title><literal>routines</literal> Columns</title>
       <entry><type>sql_identifier</type></entry>
       <entry>
        Name of the schema that the return data type of the function is
-       defined in
+       defined in.  Null for a procedure.
       </entry>
      </row>
 
@@ -4197,7 +4197,7 @@ <title><literal>routines</literal> Columns</title>
       <entry><literal>type_udt_name</literal></entry>
       <entry><type>sql_identifier</type></entry>
       <entry>
-       Name of the return data type of the function
+       Name of the return data type of the function.  Null for a procedure.
       </entry>
      </row>
 
@@ -4314,7 +4314,7 @@ <title><literal>routines</literal> Columns</title>
       <entry>
        If the function automatically returns null if any of its
        arguments are null, then <literal>YES</literal>, else
-       <literal>NO</literal>.
+       <literal>NO</literal>.  Null for a procedure.
       </entry>
      </row>
 
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 7323c2f67d..b3d72b6f95 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -5244,7 +5244,7 @@ <title>Porting a Function that Creates Another Function from <application>PL/SQL
     <para>
      Here is how this function would end up in <productname>PostgreSQL</productname>:
 <programlisting>
-CREATE OR REPLACE FUNCTION cs_update_referrer_type_proc() RETURNS void AS $func$
+CREATE OR REPLACE PROCEDURE cs_update_referrer_type_proc() AS $func$
 DECLARE
     referrer_keys CURSOR IS
         SELECT * FROM cs_referrer_keys
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 01acc2ef9d..22e6893211 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -26,8 +26,10 @@
 <!ENTITY alterOperatorClass SYSTEM "alter_opclass.sgml">
 <!ENTITY alterOperatorFamily SYSTEM "alter_opfamily.sgml">
 <!ENTITY alterPolicy        SYSTEM "alter_policy.sgml">
+<!ENTITY alterProcedure     SYSTEM "alter_procedure.sgml">
 <!ENTITY alterPublication   SYSTEM "alter_publication.sgml">
 <!ENTITY alterRole          SYSTEM "alter_role.sgml">
+<!ENTITY alterRoutine       SYSTEM "alter_routine.sgml">
 <!ENTITY alterRule          SYSTEM "alter_rule.sgml">
 <!ENTITY alterSchema        SYSTEM "alter_schema.sgml">
 <!ENTITY alterServer        SYSTEM "alter_server.sgml">
@@ -48,6 +50,7 @@
 <!ENTITY alterView          SYSTEM "alter_view.sgml">
 <!ENTITY analyze            SYSTEM "analyze.sgml">
 <!ENTITY begin              SYSTEM "begin.sgml">
+<!ENTITY call               SYSTEM "call.sgml">
 <!ENTITY checkpoint         SYSTEM "checkpoint.sgml">
 <!ENTITY close              SYSTEM "close.sgml">
 <!ENTITY cluster            SYSTEM "cluster.sgml">
@@ -75,6 +78,7 @@
 <!ENTITY createOperatorClass SYSTEM "create_opclass.sgml">
 <!ENTITY createOperatorFamily SYSTEM "create_opfamily.sgml">
 <!ENTITY createPolicy       SYSTEM "create_policy.sgml">
+<!ENTITY createProcedure    SYSTEM "create_procedure.sgml">
 <!ENTITY createPublication  SYSTEM "create_publication.sgml">
 <!ENTITY createRole         SYSTEM "create_role.sgml">
 <!ENTITY createRule         SYSTEM "create_rule.sgml">
@@ -122,8 +126,10 @@
 <!ENTITY dropOperatorFamily  SYSTEM "drop_opfamily.sgml">
 <!ENTITY dropOwned          SYSTEM "drop_owned.sgml">
 <!ENTITY dropPolicy         SYSTEM "drop_policy.sgml">
+<!ENTITY dropProcedure      SYSTEM "drop_procedure.sgml">
 <!ENTITY dropPublication    SYSTEM "drop_publication.sgml">
 <!ENTITY dropRole           SYSTEM "drop_role.sgml">
+<!ENTITY dropRoutine        SYSTEM "drop_routine.sgml">
 <!ENTITY dropRule           SYSTEM "drop_rule.sgml">
 <!ENTITY dropSchema         SYSTEM "drop_schema.sgml">
 <!ENTITY dropSequence       SYSTEM "drop_sequence.sgml">
diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml
index bc7401f845..42ec60a680 100644
--- a/doc/src/sgml/ref/alter_default_privileges.sgml
+++ b/doc/src/sgml/ref/alter_default_privileges.sgml
@@ -39,7 +39,7 @@
     TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ]
 
 GRANT { EXECUTE | ALL [ PRIVILEGES ] }
-    ON FUNCTIONS
+    ON { FUNCTIONS | ROUTINES }
     TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ]
 
 GRANT { USAGE | ALL [ PRIVILEGES ] }
@@ -66,7 +66,7 @@
 
 REVOKE [ GRANT OPTION FOR ]
     { EXECUTE | ALL [ PRIVILEGES ] }
-    ON FUNCTIONS
+    ON { FUNCTIONS | ROUTINES }
     FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
     [ CASCADE | RESTRICT ]
 
@@ -93,7 +93,9 @@ <title>Description</title>
    affect privileges assigned to already-existing objects.)  Currently,
    only the privileges for schemas, tables (including views and foreign
    tables), sequences, functions, and types (including domains) can be
-   altered.
+   altered.  For this command, functions include aggregates and procedures.
+   The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are
+   equivalent in this command.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_extension.sgml b/doc/src/sgml/ref/alter_extension.sgml
index c2b0669c38..d83b0f1dbb 100644
--- a/doc/src/sgml/ref/alter_extension.sgml
+++ b/doc/src/sgml/ref/alter_extension.sgml
@@ -45,6 +45,8 @@
   OPERATOR CLASS <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   OPERATOR FAMILY <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
+  PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
+  ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   SCHEMA <replaceable class="parameter">object_name</replaceable> |
   SEQUENCE <replaceable class="parameter">object_name</replaceable> |
   SERVER <replaceable class="parameter">object_name</replaceable> |
@@ -170,12 +172,14 @@ <title>Parameters</title>
      <term><replaceable class="parameter">aggregate_name</replaceable></term>
      <term><replaceable class="parameter">function_name</replaceable></term>
      <term><replaceable class="parameter">operator_name</replaceable></term>
+     <term><replaceable class="parameter">procedure_name</replaceable></term>
+     <term><replaceable class="parameter">routine_name</replaceable></term>
      <listitem>
       <para>
        The name of an object to be added to or removed from the extension.
        Names of tables,
        aggregates, domains, foreign tables, functions, operators,
-       operator classes, operator families, sequences, text search objects,
+       operator classes, operator families, procedures, routines, sequences, text search objects,
        types, and views can be schema-qualified.
       </para>
      </listitem>
@@ -204,7 +208,7 @@ <title>Parameters</title>
 
      <listitem>
       <para>
-       The mode of a function or aggregate
+       The mode of a function, procedure, or aggregate
        argument: <literal>IN</literal>, <literal>OUT</literal>,
        <literal>INOUT</literal>, or <literal>VARIADIC</literal>.
        If omitted, the default is <literal>IN</literal>.
@@ -222,7 +226,7 @@ <title>Parameters</title>
 
      <listitem>
       <para>
-       The name of a function or aggregate argument.
+       The name of a function, procedure, or aggregate argument.
        Note that <command>ALTER EXTENSION</command> does not actually pay
        any attention to argument names, since only the argument data
        types are needed to determine the function's identity.
@@ -235,7 +239,7 @@ <title>Parameters</title>
 
      <listitem>
       <para>
-       The data type of a function or aggregate argument.
+       The data type of a function, procedure, or aggregate argument.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/alter_function.sgml b/doc/src/sgml/ref/alter_function.sgml
index fd35e98a88..847e300bd4 100644
--- a/doc/src/sgml/ref/alter_function.sgml
+++ b/doc/src/sgml/ref/alter_function.sgml
@@ -359,6 +359,8 @@ <title>See Also</title>
   <simplelist type="inline">
    <member><xref linkend="sql-createfunction"></member>
    <member><xref linkend="sql-dropfunction"></member>
+   <member><xref linkend="sql-alterprocedure"></member>
+   <member><xref linkend="sql-alterroutine"></member>
   </simplelist>
  </refsect1>
 </refentry>
diff --git a/doc/src/sgml/ref/alter_procedure.sgml b/doc/src/sgml/ref/alter_procedure.sgml
new file mode 100644
index 0000000000..c8fa9451c5
--- /dev/null
+++ b/doc/src/sgml/ref/alter_procedure.sgml
@@ -0,0 +1,303 @@
+<!--
+doc/src/sgml/ref/alter_procedure.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterprocedure">
+ <indexterm zone="sql-alterprocedure">
+  <primary>ALTER PROCEDURE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER PROCEDURE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER PROCEDURE</refname>
+  <refpurpose>change the definition of a procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    <replaceable class="parameter">action</replaceable> [ ... ] [ RESTRICT ]
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    RENAME TO <replaceable>new_name</replaceable>
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_USER | SESSION_USER }
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    SET SCHEMA <replaceable>new_schema</replaceable>
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    DEPENDS ON EXTENSION <replaceable>extension_name</replaceable>
+
+<phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
+
+    IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF
+    [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
+    PARALLEL { UNSAFE | RESTRICTED | SAFE }
+    COST <replaceable class="parameter">execution_cost</replaceable>
+    ROWS <replaceable class="parameter">result_rows</replaceable>
+    SET <replaceable class="parameter">configuration_parameter</replaceable> { TO | = } { <replaceable class="parameter">value</replaceable> | DEFAULT }
+    SET <replaceable class="parameter">configuration_parameter</replaceable> FROM CURRENT
+    RESET <replaceable class="parameter">configuration_parameter</replaceable>
+    RESET ALL
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER PROCEDURE</command> changes the definition of a
+   procedure.
+  </para>
+
+  <para>
+   You must own the procedure to use <command>ALTER PROCEDURE</command>.
+   To change a procedure's schema, you must also have <literal>CREATE</literal>
+   privilege on the new schema.
+   To alter the owner, you must also be a direct or indirect member of the new
+   owning role, and that role must have <literal>CREATE</literal> privilege on
+   the procedure's schema.  (These restrictions enforce that altering the owner
+   doesn't do anything you couldn't do by dropping and recreating the procedure.
+   However, a superuser can alter ownership of any procedure anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing procedure.  If no
+      argument list is specified, the name must be unique in its schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argmode</replaceable></term>
+
+    <listitem>
+     <para>
+      The mode of an argument: <literal>IN</literal>  or <literal>VARIADIC</literal>.
+      If omitted, the default is <literal>IN</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argname</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of an argument.
+      Note that <command>ALTER PROCEDURE</command> does not actually pay
+      any attention to argument names, since only the argument data
+      types are needed to determine the procedure's identity.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argtype</replaceable></term>
+
+    <listitem>
+     <para>
+      The data type(s) of the procedure's arguments (optionally
+      schema-qualified), if any.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the procedure.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the procedure.  Note that if the procedure is
+      marked <literal>SECURITY DEFINER</literal>, it will subsequently
+      execute as the new owner.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the procedure.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">extension_name</replaceable></term>
+    <listitem>
+     <para>
+      The name of the extension that the procedure is to depend on.
+     </para>
+    </listitem>
+   </varlistentry>
+
+    <varlistentry>
+     <term><literal>IMMUTABLE</literal></term>
+     <term><literal>STABLE</literal></term>
+     <term><literal>VOLATILE</literal></term>
+     <term><literal>PARALLEL</literal></term>
+     <term><literal>LEAKPROOF</literal></term>
+     <term><literal>COST</literal> <replaceable class="parameter">execution_cost</replaceable></term>
+     <term><literal>ROWS</literal> <replaceable class="parameter">result_rows</replaceable></term>
+
+     <listitem>
+      <para>
+       These attributes are accepted for compatibility
+       with <xref linkend="sql-alterfunction"> but are currently not used for
+       procedures.
+      </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal><optional> EXTERNAL </optional> SECURITY INVOKER</literal></term>
+    <term><literal><optional> EXTERNAL </optional> SECURITY DEFINER</literal></term>
+
+    <listitem>
+     <para>
+      Change whether the procedure is a security definer or not. The
+      key word <literal>EXTERNAL</literal> is ignored for SQL
+      conformance. See <xref linkend="sql-createprocedure"> for more information about
+      this capability.
+     </para>
+    </listitem>
+   </varlistentry>
+
+     <varlistentry>
+      <term><replaceable>configuration_parameter</replaceable></term>
+      <term><replaceable>value</replaceable></term>
+      <listitem>
+       <para>
+        Add or change the assignment to be made to a configuration parameter
+        when the procedure is called.  If
+        <replaceable>value</replaceable> is <literal>DEFAULT</literal>
+        or, equivalently, <literal>RESET</literal> is used, the procedure-local
+        setting is removed, so that the procedure executes with the value
+        present in its environment.  Use <literal>RESET
+        ALL</literal> to clear all procedure-local settings.
+        <literal>SET FROM CURRENT</literal> saves the value of the parameter that
+        is current when <command>ALTER PROCEDURE</command> is executed as the value
+        to be applied when the procedure is entered.
+       </para>
+
+       <para>
+        See <xref linkend="sql-set"> and
+        <xref linkend="runtime-config">
+        for more information about allowed parameter names and values.
+       </para>
+      </listitem>
+     </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+
+    <listitem>
+     <para>
+      Ignored for conformance with the SQL standard.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the procedure <literal>insert_data</literal> with two arguments
+   of type <type>integer</type> to <literal>insert_record</literal>:
+<programlisting>
+ALTER PROCEDURE insert_data(integer, integer) RENAME TO insert_record;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the procedure <literal>insert_data</literal> with
+   two arguments of type <type>integer</type> to <literal>joe</literal>:
+<programlisting>
+ALTER PROCEDURE insert_data(integer, integer) OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To change the schema of the procedure <literal>insert_data</literal> with
+   two arguments of type <type>integer</type>
+   to <literal>accounting</literal>:
+<programlisting>
+ALTER PROCEDURE insert_data(integer, integer) SET SCHEMA accounting;
+</programlisting>
+  </para>
+
+  <para>
+   To mark the procedure <literal>insert_data(integer, integer)</literal> as
+   being dependent on the extension <literal>myext</literal>:
+<programlisting>
+ALTER PROCEDURE insert_data(integer, integer) DEPENDS ON EXTENSION myext;
+</programlisting>
+  </para>
+
+  <para>
+   To adjust the search path that is automatically set for a procedure:
+<programlisting>
+ALTER PROCEDURE check_password(text) SET search_path = admin, pg_temp;
+</programlisting>
+  </para>
+
+  <para>
+   To disable automatic setting of <varname>search_path</varname> for a procedure:
+<programlisting>
+ALTER PROCEDURE check_password(text) RESET search_path;
+</programlisting>
+   The procedure will now execute with whatever search path is used by its
+   caller.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   This statement is partially compatible with the <command>ALTER
+   PROCEDURE</command> statement in the SQL standard. The standard allows more
+   properties of a procedure to be modified, but does not provide the
+   ability to rename a procedure, make a procedure a security definer,
+   attach configuration parameter values to a procedure,
+   or change the owner, schema, or volatility of a procedure. The standard also
+   requires the <literal>RESTRICT</literal> key word, which is optional in
+   <productname>PostgreSQL</productname>.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createprocedure"></member>
+   <member><xref linkend="sql-dropprocedure"></member>
+   <member><xref linkend="sql-alterfunction"></member>
+   <member><xref linkend="sql-alterroutine"></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_routine.sgml b/doc/src/sgml/ref/alter_routine.sgml
new file mode 100644
index 0000000000..d02b5618dc
--- /dev/null
+++ b/doc/src/sgml/ref/alter_routine.sgml
@@ -0,0 +1,102 @@
+<!--
+doc/src/sgml/ref/alter_routine.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterroutine">
+ <indexterm zone="sql-alterroutine">
+  <primary>ALTER ROUTINE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER ROUTINE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER ROUTINE</refname>
+  <refpurpose>change the definition of a routine</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    <replaceable class="parameter">action</replaceable> [ ... ] [ RESTRICT ]
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    RENAME TO <replaceable>new_name</replaceable>
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_USER | SESSION_USER }
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    SET SCHEMA <replaceable>new_schema</replaceable>
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    DEPENDS ON EXTENSION <replaceable>extension_name</replaceable>
+
+<phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
+
+    IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF
+    [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
+    PARALLEL { UNSAFE | RESTRICTED | SAFE }
+    COST <replaceable class="parameter">execution_cost</replaceable>
+    ROWS <replaceable class="parameter">result_rows</replaceable>
+    SET <replaceable class="parameter">configuration_parameter</replaceable> { TO | = } { <replaceable class="parameter">value</replaceable> | DEFAULT }
+    SET <replaceable class="parameter">configuration_parameter</replaceable> FROM CURRENT
+    RESET <replaceable class="parameter">configuration_parameter</replaceable>
+    RESET ALL
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER ROUTINE</command> changes the definition of a routine, which
+   can be an aggregate function, a normal function, or a procedure.  See
+   under <xref linkend="sql-alteraggregate">, <xref linkend="sql-alterfunction">,
+   and <xref linkend="sql-alterprocedure"> for the description of the
+   parameters, more examples, and further details.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the routine <literal>foo</literal> for type
+   <type>integer</type> to <literal>foobar</literal>:
+<programlisting>
+ALTER ROUTINE foo(integer) RENAME TO foobar;
+</programlisting>
+   This command will work independent of whether <literal>foo</literal> is an
+   aggregate, function, or procedure.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   This statement is partially compatible with the <command>ALTER
+   ROUTINE</command> statement in the SQL standard.  See
+   under <xref linkend="sql-alterfunction">
+   and <xref linkend="sql-alterprocedure"> for more details.  Allowing
+   routine names to refer to aggregate functions is
+   a <productname>PostgreSQL</productname> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alteraggregate"></member>
+   <member><xref linkend="sql-alterfunction"></member>
+   <member><xref linkend="sql-alterprocedure"></member>
+   <member><xref linkend="sql-droproutine"></member>
+  </simplelist>
+
+  <para>
+   Note that there is no <literal>CREATE ROUTINE</literal> command.
+  </para>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/call.sgml b/doc/src/sgml/ref/call.sgml
new file mode 100644
index 0000000000..a0f7e6d888
--- /dev/null
+++ b/doc/src/sgml/ref/call.sgml
@@ -0,0 +1,97 @@
+<!--
+doc/src/sgml/ref/call.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-call">
+ <indexterm zone="sql-call">
+  <primary>CALL</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CALL</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CALL</refname>
+  <refpurpose>invoke a procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CALL <replaceable class="parameter">name</replaceable> ( [ <replaceable class="parameter">argument</replaceable> ] [ , ...] )
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CALL</command> executes a procedure.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the procedure.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  <varlistentry>
+    <term><replaceable class="parameter">argument</replaceable></term>
+    <listitem>
+     <para>
+      An argument for the procedure call.
+      See <xref linkend="sql-syntax-calling-funcs"> for the full details on
+      function and procedure call syntax, including use of named parameters.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The user must have <literal>EXECUTE</literal> privilege on the procedure in
+   order to be allowed to invoke it.
+  </para>
+
+  <para>
+   To call a function (not a procedure), use <command>SELECT</command> instead.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+<programlisting>
+CALL do_db_maintenance();
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CALL</command> conforms to the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createprocedure"></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index d705792a45..1c2aa2a2b5 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -46,8 +46,10 @@
   OPERATOR FAMILY <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   POLICY <replaceable class="parameter">policy_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
+  PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   PUBLICATION <replaceable class="parameter">object_name</replaceable> |
   ROLE <replaceable class="parameter">object_name</replaceable> |
+  ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   RULE <replaceable class="parameter">rule_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   SCHEMA <replaceable class="parameter">object_name</replaceable> |
   SEQUENCE <replaceable class="parameter">object_name</replaceable> |
@@ -121,13 +123,15 @@ <title>Parameters</title>
     <term><replaceable class="parameter">function_name</replaceable></term>
     <term><replaceable class="parameter">operator_name</replaceable></term>
     <term><replaceable class="parameter">policy_name</replaceable></term>
+    <term><replaceable class="parameter">procedure_name</replaceable></term>
+    <term><replaceable class="parameter">routine_name</replaceable></term>
     <term><replaceable class="parameter">rule_name</replaceable></term>
     <term><replaceable class="parameter">trigger_name</replaceable></term>
     <listitem>
      <para>
       The name of the object to be commented.  Names of tables,
       aggregates, collations, conversions, domains, foreign tables, functions,
-      indexes, operators, operator classes, operator families, sequences,
+      indexes, operators, operator classes, operator families, procedures, routines, sequences,
       statistics, text search objects, types, and views can be
       schema-qualified. When commenting on a column,
       <replaceable class="parameter">relation_name</replaceable> must refer
@@ -170,7 +174,7 @@ <title>Parameters</title>
     <term><replaceable class="parameter">argmode</replaceable></term>
     <listitem>
      <para>
-      The mode of a function or aggregate
+      The mode of a function, procedure, or aggregate
       argument: <literal>IN</literal>, <literal>OUT</literal>,
       <literal>INOUT</literal>, or <literal>VARIADIC</literal>.
       If omitted, the default is <literal>IN</literal>.
@@ -187,7 +191,7 @@ <title>Parameters</title>
     <term><replaceable class="parameter">argname</replaceable></term>
     <listitem>
      <para>
-      The name of a function or aggregate argument.
+      The name of a function, procedure, or aggregate argument.
       Note that <command>COMMENT</command> does not actually pay
       any attention to argument names, since only the argument data
       types are needed to determine the function's identity.
@@ -199,7 +203,7 @@ <title>Parameters</title>
     <term><replaceable class="parameter">argtype</replaceable></term>
     <listitem>
      <para>
-      The data type of a function or aggregate argument.
+      The data type of a function, procedure, or aggregate argument.
      </para>
     </listitem>
    </varlistentry>
@@ -325,6 +329,7 @@ <title>Examples</title>
 COMMENT ON OPERATOR CLASS int4ops USING btree IS '4 byte integer operators for btrees';
 COMMENT ON OPERATOR FAMILY integer_ops USING btree IS 'all integer operators for btrees';
 COMMENT ON POLICY my_policy ON mytable IS 'Filter rows by users';
+COMMENT ON PROCEDURE my_proc (integer, integer) IS 'Runs a report';
 COMMENT ON ROLE my_role IS 'Administration group for finance tables';
 COMMENT ON RULE my_rule ON my_table IS 'Logs updates of employee records';
 COMMENT ON SCHEMA my_schema IS 'Departmental data';
diff --git a/doc/src/sgml/ref/create_function.sgml b/doc/src/sgml/ref/create_function.sgml
index 970dc13359..5c8d1b7e1b 100644
--- a/doc/src/sgml/ref/create_function.sgml
+++ b/doc/src/sgml/ref/create_function.sgml
@@ -55,7 +55,7 @@ <title>Description</title>
   <para>
    If a schema name is included, then the function is created in the
    specified schema.  Otherwise it is created in the current schema.
-   The name of the new function must not match any existing function
+   The name of the new function must not match any existing function or procedure
    with the same input argument types in the same schema.  However,
    functions of different argument types can share a name (this is
    called <firstterm>overloading</firstterm>).
@@ -450,7 +450,7 @@ <title>Parameters</title>
    </varlistentry>
 
     <varlistentry>
-     <term><replaceable class="parameter">execution_cost</replaceable></term>
+     <term><literal>COST</literal> <replaceable class="parameter">execution_cost</replaceable></term>
 
      <listitem>
       <para>
@@ -466,7 +466,7 @@ <title>Parameters</title>
     </varlistentry>
 
     <varlistentry>
-     <term><replaceable class="parameter">result_rows</replaceable></term>
+     <term><literal>ROWS</literal> <replaceable class="parameter">result_rows</replaceable></term>
 
      <listitem>
       <para>
@@ -818,7 +818,7 @@ <title>Writing <literal>SECURITY DEFINER</literal> Functions Safely</title>
   <title>Compatibility</title>
 
   <para>
-   A <command>CREATE FUNCTION</command> command is defined in SQL:1999 and later.
+   A <command>CREATE FUNCTION</command> command is defined in the SQL standard.
    The <productname>PostgreSQL</productname> version is similar but
    not fully compatible.  The attributes are not portable, neither are the
    different available languages.
diff --git a/doc/src/sgml/ref/create_procedure.sgml b/doc/src/sgml/ref/create_procedure.sgml
new file mode 100644
index 0000000000..b2948750a8
--- /dev/null
+++ b/doc/src/sgml/ref/create_procedure.sgml
@@ -0,0 +1,363 @@
+<!--
+doc/src/sgml/ref/create_procedure.sgml
+-->
+
+<refentry id="sql-createprocedure">
+ <indexterm zone="sql-createprocedure">
+  <primary>CREATE PROCEDURE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE PROCEDURE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE PROCEDURE</refname>
+  <refpurpose>define a new procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE [ OR REPLACE ] PROCEDURE
+    <replaceable class="parameter">name</replaceable> ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [ { DEFAULT | = } <replaceable class="parameter">default_expr</replaceable> ] [, ...] ] )
+  { LANGUAGE <replaceable class="parameter">lang_name</replaceable>
+    | TRANSFORM { FOR TYPE <replaceable class="parameter">type_name</replaceable> } [, ... ]
+    | IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF
+    | [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
+    | PARALLEL { UNSAFE | RESTRICTED | SAFE }
+    | COST <replaceable class="parameter">execution_cost</replaceable>
+    | ROWS <replaceable class="parameter">result_rows</replaceable>
+    | SET <replaceable class="parameter">configuration_parameter</replaceable> { TO <replaceable class="parameter">value</replaceable> | = <replaceable class="parameter">value</replaceable> | FROM CURRENT }
+    | AS '<replaceable class="parameter">definition</replaceable>'
+    | AS '<replaceable class="parameter">obj_file</replaceable>', '<replaceable class="parameter">link_symbol</replaceable>'
+  } ...
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="sql-createprocedure-description">
+  <title>Description</title>
+
+  <para>
+   <command>CREATE PROCEDURE</command> defines a new procedure.
+   <command>CREATE OR REPLACE PROCEDURE</command> will either create a
+   new procedure, or replace an existing definition.
+   To be able to define a procedure, the user must have the
+   <literal>USAGE</literal> privilege on the language.
+  </para>
+
+  <para>
+   If a schema name is included, then the procedure is created in the
+   specified schema.  Otherwise it is created in the current schema.
+   The name of the new procedure must not match any existing procedure or function
+   with the same input argument types in the same schema.  However,
+   procedures of different argument types can share a name (this is
+   called <firstterm>overloading</firstterm>).
+  </para>
+
+  <para>
+   To replace the current definition of an existing procedure, use
+   <command>CREATE OR REPLACE PROCEDURE</command>.  It is not possible
+   to change the name or argument types of a procedure this way (if you
+   tried, you would actually be creating a new, distinct procedure).
+  </para>
+
+  <para>
+   When <command>CREATE OR REPLACE PROCEDURE</command> is used to replace an
+   existing procedure, the ownership and permissions of the procedure
+   do not change.  All other procedure properties are assigned the
+   values specified or implied in the command.  You must own the procedure
+   to replace it (this includes being a member of the owning role).
+  </para>
+
+  <para>
+   The user that creates the procedure becomes the owner of the procedure.
+  </para>
+
+  <para>
+   To be able to create a procedure, you must have <literal>USAGE</literal>
+   privilege on the argument types.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><replaceable class="parameter">name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the procedure to create.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">argmode</replaceable></term>
+
+     <listitem>
+      <para>
+       The mode of an argument: <literal>IN</literal> or <literal>VARIADIC</literal>.
+       If omitted, the default is <literal>IN</literal>.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">argname</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of an argument.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">argtype</replaceable></term>
+
+     <listitem>
+      <para>
+       The data type(s) of the procedure's arguments (optionally
+       schema-qualified), if any. The argument types can be base, composite,
+       or domain types, or can reference the type of a table column.
+      </para>
+      <para>
+       Depending on the implementation language it might also be allowed
+       to specify <quote>pseudo-types</quote> such as <type>cstring</type>.
+       Pseudo-types indicate that the actual argument type is either
+       incompletely specified, or outside the set of ordinary SQL data types.
+      </para>
+      <para>
+       The type of a column is referenced by writing
+       <literal><replaceable
+       class="parameter">table_name</replaceable>.<replaceable
+       class="parameter">column_name</replaceable>%TYPE</literal>.
+       Using this feature can sometimes help make a procedure independent of
+       changes to the definition of a table.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">default_expr</replaceable></term>
+
+     <listitem>
+      <para>
+       An expression to be used as default value if the parameter is
+       not specified.  The expression has to be coercible to the
+       argument type of the parameter.
+       All input parameters following a
+       parameter with a default value must have default values as well.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">lang_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the language that the procedure is implemented in.
+       It can be <literal>sql</literal>, <literal>c</literal>,
+       <literal>internal</literal>, or the name of a user-defined
+       procedural language, e.g. <literal>plpgsql</literal>.  Enclosing the
+       name in single quotes is deprecated and requires matching case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>TRANSFORM { FOR TYPE <replaceable class="parameter">type_name</replaceable> } [, ... ] }</literal></term>
+
+     <listitem>
+      <para>
+       Lists which transforms a call to the procedure should apply.  Transforms
+       convert between SQL types and language-specific data types;
+       see <xref linkend="sql-createtransform">.  Procedural language
+       implementations usually have hardcoded knowledge of the built-in types,
+       so those don't need to be listed here.  If a procedural language
+       implementation does not know how to handle a type and no transform is
+       supplied, it will fall back to a default behavior for converting data
+       types, but this depends on the implementation.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>IMMUTABLE</literal></term>
+     <term><literal>STABLE</literal></term>
+     <term><literal>VOLATILE</literal></term>
+     <term><literal>LEAKPROOF</literal></term>
+     <term><literal>PARALLEL</literal></term>
+     <term><literal>COST</literal> <replaceable class="parameter">execution_cost</replaceable></term>
+     <term><literal>ROWS</literal> <replaceable class="parameter">result_rows</replaceable></term>
+
+     <listitem>
+      <para>
+       These attributes are accepted for compatibility
+       with <xref linkend="sql-createfunction"> but are currently not used for
+       procedures.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   <varlistentry>
+    <term><literal><optional>EXTERNAL</optional> SECURITY INVOKER</literal></term>
+    <term><literal><optional>EXTERNAL</optional> SECURITY DEFINER</literal></term>
+
+    <listitem>
+     <para><literal>SECURITY INVOKER</literal> indicates that the procedure
+      is to be executed with the privileges of the user that calls it.
+      That is the default.  <literal>SECURITY DEFINER</literal>
+      specifies that the procedure is to be executed with the
+      privileges of the user that owns it.
+     </para>
+
+     <para>
+      The key word <literal>EXTERNAL</literal> is allowed for SQL
+      conformance, but it is optional since, unlike in SQL, this feature
+      applies to all procedures not only external ones.
+     </para>
+    </listitem>
+   </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>configuration_parameter</replaceable></term>
+     <term><replaceable>value</replaceable></term>
+     <listitem>
+      <para>
+       The <literal>SET</literal> clause causes the specified configuration
+       parameter to be set to the specified value when the procedure is
+       entered, and then restored to its prior value when the procedure exits.
+       <literal>SET FROM CURRENT</literal> saves the value of the parameter that
+       is current when <command>CREATE PROCEDURE</command> is executed as the value
+       to be applied when the procedure is entered.
+      </para>
+
+      <para>
+       If a <literal>SET</literal> clause is attached to a procedure, then
+       the effects of a <command>SET LOCAL</command> command executed inside the
+       procedure for the same variable are restricted to the procedure: the
+       configuration parameter's prior value is still restored at procedure exit.
+       However, an ordinary
+       <command>SET</command> command (without <literal>LOCAL</literal>) overrides the
+       <literal>SET</literal> clause, much as it would do for a previous <command>SET
+       LOCAL</command> command: the effects of such a command will persist after
+       procedure exit, unless the current transaction is rolled back.
+      </para>
+
+      <para>
+       See <xref linkend="sql-set"> and
+       <xref linkend="runtime-config">
+       for more information about allowed parameter names and values.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">definition</replaceable></term>
+
+     <listitem>
+      <para>
+       A string constant defining the procedure; the meaning depends on the
+       language.  It can be an internal procedure name, the path to an
+       object file, an SQL command, or text in a procedural language.
+      </para>
+
+      <para>
+       It is often helpful to use dollar quoting (see <xref
+       linkend="sql-syntax-dollar-quoting">) to write the procedure definition
+       string, rather than the normal single quote syntax.  Without dollar
+       quoting, any single quotes or backslashes in the procedure definition must
+       be escaped by doubling them.
+      </para>
+
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal><replaceable class="parameter">obj_file</replaceable>, <replaceable class="parameter">link_symbol</replaceable></literal></term>
+
+     <listitem>
+      <para>
+       This form of the <literal>AS</literal> clause is used for
+       dynamically loadable C language procedures when the procedure name
+       in the C language source code is not the same as the name of
+       the SQL procedure. The string <replaceable
+       class="parameter">obj_file</replaceable> is the name of the shared
+       library file containing the compiled C procedure, and is interpreted
+       as for the <xref linkend="SQL-LOAD"> command.  The string
+       <replaceable class="parameter">link_symbol</replaceable> is the
+       procedure's link symbol, that is, the name of the procedure in the C
+       language source code.  If the link symbol is omitted, it is assumed
+       to be the same as the name of the SQL procedure being defined.
+      </para>
+
+      <para>
+       When repeated <command>CREATE PROCEDURE</command> calls refer to
+       the same object file, the file is only loaded once per session.
+       To unload and
+       reload the file (perhaps during development), start a new session.
+      </para>
+
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-createprocedure-notes">
+  <title>Notes</title>
+
+  <para>
+   See <xref linkend="sql-createfunction"> for more details on function
+   creation that also apply to procedures.
+  </para>
+
+  <para>
+   Use <xref linkend="sql-call"> to execute a procedure.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-createprocedure-examples">
+  <title>Examples</title>
+
+<programlisting>
+CREATE PROCEDURE insert_data(a integer, b integer)
+LANGUAGE SQL
+AS $$
+INSERT INTO tbl VALUES (a);
+INSERT INTO tbl VALUES (b);
+$$;
+
+CALL insert_data(1, 2);
+</programlisting>
+ </refsect1>
+
+ <refsect1 id="sql-createprocedure-compat">
+  <title>Compatibility</title>
+
+  <para>
+   A <command>CREATE PROCEDURE</command> command is defined in the SQL
+   standard.  The <productname>PostgreSQL</productname> version is similar but
+   not fully compatible.  For details see
+   also <xref linkend="sql-createfunction">.
+  </para>
+ </refsect1>
+
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterprocedure"></member>
+   <member><xref linkend="sql-dropprocedure"></member>
+   <member><xref linkend="sql-call"></member>
+   <member><xref linkend="sql-createfunction"></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_function.sgml b/doc/src/sgml/ref/drop_function.sgml
index 05b405dda1..b926ca4839 100644
--- a/doc/src/sgml/ref/drop_function.sgml
+++ b/doc/src/sgml/ref/drop_function.sgml
@@ -185,6 +185,8 @@ <title>See Also</title>
   <simplelist type="inline">
    <member><xref linkend="sql-createfunction"></member>
    <member><xref linkend="sql-alterfunction"></member>
+   <member><xref linkend="sql-dropprocedure"></member>
+   <member><xref linkend="sql-droproutine"></member>
   </simplelist>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/drop_procedure.sgml b/doc/src/sgml/ref/drop_procedure.sgml
new file mode 100644
index 0000000000..c091822705
--- /dev/null
+++ b/doc/src/sgml/ref/drop_procedure.sgml
@@ -0,0 +1,162 @@
+<!--
+doc/src/sgml/ref/drop_procedure.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropprocedure">
+ <indexterm zone="sql-dropprocedure">
+  <primary>DROP PROCEDURE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP PROCEDURE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP PROCEDURE</refname>
+  <refpurpose>remove a procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP PROCEDURE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] [, ...]
+    [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP PROCEDURE</command> removes the definition of an existing
+   procedure. To execute this command the user must be the
+   owner of the procedure. The argument types to the
+   procedure must be specified, since several different procedures
+   can exist with the same name and different argument lists.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+    <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the procedure does not exist. A notice is issued
+      in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing procedure.  If no
+      argument list is specified, the name must be unique in its schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argmode</replaceable></term>
+
+    <listitem>
+     <para>
+      The mode of an argument: <literal>IN</literal> or <literal>VARIADIC</literal>.
+      If omitted, the default is <literal>IN</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argname</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of an argument.
+      Note that <command>DROP PROCEDURE</command> does not actually pay
+      any attention to argument names, since only the argument data
+      types are needed to determine the procedure's identity.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argtype</replaceable></term>
+
+    <listitem>
+     <para>
+      The data type(s) of the procedure's arguments (optionally
+      schema-qualified), if any.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the procedure,
+      and in turn all objects that depend on those objects
+      (see <xref linkend="ddl-depend">).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the procedure if any objects depend on it.  This
+      is the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-dropprocedure-examples">
+  <title>Examples</title>
+
+<programlisting>
+DROP PROCEDURE do_db_maintenance();
+</programlisting>
+ </refsect1>
+
+ <refsect1 id="sql-dropprocedure-compatibility">
+  <title>Compatibility</title>
+
+  <para>
+   This command conforms to the SQL standard, with
+   these <productname>PostgreSQL</productname> extensions:
+   <itemizedlist>
+    <listitem>
+     <para>The standard only allows one procedure to be dropped per command.</para>
+    </listitem>
+    <listitem>
+     <para>The <literal>IF EXISTS</literal> option</para>
+    </listitem>
+    <listitem>
+     <para>The ability to specify argument modes and names</para>
+    </listitem>
+   </itemizedlist>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createprocedure"></member>
+   <member><xref linkend="sql-alterprocedure"></member>
+   <member><xref linkend="sql-dropfunction"></member>
+   <member><xref linkend="sql-droproutine"></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_routine.sgml b/doc/src/sgml/ref/drop_routine.sgml
new file mode 100644
index 0000000000..feb4bb3187
--- /dev/null
+++ b/doc/src/sgml/ref/drop_routine.sgml
@@ -0,0 +1,94 @@
+<!--
+doc/src/sgml/ref/drop_routine.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-droproutine">
+ <indexterm zone="sql-droproutine">
+  <primary>DROP ROUTINE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP ROUTINE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP ROUTINE</refname>
+  <refpurpose>remove a routine</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP ROUTINE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] [, ...]
+    [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP ROUTINE</command> removes the definition of an existing
+   routine, which can be an aggregate function, a normal function, or a
+   procedure.  See
+   under <xref linkend="sql-dropaggregate">, <xref linkend="sql-dropfunction">,
+   and <xref linkend="sql-dropprocedure"> for the description of the
+   parameters, more examples, and further details.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-droproutine-examples">
+  <title>Examples</title>
+
+  <para>
+   To drop the routine <literal>foo</literal> for type
+   <type>integer</type>:
+<programlisting>
+DROP ROUTINE foo(integer);
+</programlisting>
+   This command will work independent of whether <literal>foo</literal> is an
+   aggregate, function, or procedure.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-droproutine-compatibility">
+  <title>Compatibility</title>
+
+  <para>
+   This command conforms to the SQL standard, with
+   these <productname>PostgreSQL</productname> extensions:
+   <itemizedlist>
+    <listitem>
+     <para>The standard only allows one routine to be dropped per command.</para>
+    </listitem>
+    <listitem>
+     <para>The <literal>IF EXISTS</literal> option</para>
+    </listitem>
+    <listitem>
+     <para>The ability to specify argument modes and names</para>
+    </listitem>
+    <listitem>
+     <para>Aggregate functions are an extension.</para>
+    </listitem>
+   </itemizedlist>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-dropaggregate"></member>
+   <member><xref linkend="sql-dropfunction"></member>
+   <member><xref linkend="sql-dropprocedure"></member>
+   <member><xref linkend="sql-alterroutine"></member>
+  </simplelist>
+
+  <para>
+   Note that there is no <literal>CREATE ROUTINE</literal> command.
+  </para>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 475c85b835..39738c28ae 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -55,8 +55,8 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
 
 GRANT { EXECUTE | ALL [ PRIVILEGES ] }
-    ON { FUNCTION <replaceable>function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
-         | ALL FUNCTIONS IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] }
+    ON { { FUNCTION | PROCEDURE | ROUTINE } <replaceable>routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
+         | ALL { FUNCTIONS | PROCEDURES | ROUTINES } IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] }
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
 
 GRANT { USAGE | ALL [ PRIVILEGES ] }
@@ -96,7 +96,7 @@ <title>Description</title>
   <para>
    The <command>GRANT</command> command has two basic variants: one
    that grants privileges on a database object (table, column, view, foreign
-   table, sequence, database, foreign-data wrapper, foreign server, function,
+   table, sequence, database, foreign-data wrapper, foreign server, function, procedure,
    procedural language, schema, or tablespace), and one that grants
    membership in a role.  These variants are similar in many ways, but
    they are different enough to be described separately.
@@ -115,8 +115,11 @@ <title>GRANT on Database Objects</title>
   <para>
    There is also an option to grant privileges on all objects of the same
    type within one or more schemas.  This functionality is currently supported
-   only for tables, sequences, and functions (but note that <literal>ALL
-   TABLES</literal> is considered to include views and foreign tables).
+   only for tables, sequences, functions, and procedures.  <literal>ALL
+   TABLES</literal> also affects views and foreign tables, just like the
+   specific-object <command>GRANT</command> command.  <literal>ALL
+   FUNCTIONS</literal> also affects aggregate functions, but not procedures,
+   again just like the specific-object <command>GRANT</command> command.
   </para>
 
   <para>
@@ -169,7 +172,7 @@ <title>GRANT on Database Objects</title>
    granted to <literal>PUBLIC</literal> are as follows:
    <literal>CONNECT</literal> and <literal>TEMPORARY</literal> (create
    temporary tables) privileges for databases;
-   <literal>EXECUTE</literal> privilege for functions; and
+   <literal>EXECUTE</literal> privilege for functions and procedures; and
    <literal>USAGE</literal> privilege for languages and data types
    (including domains).
    The object owner can, of course, <command>REVOKE</command>
@@ -329,10 +332,12 @@ <title>GRANT on Database Objects</title>
      <term><literal>EXECUTE</literal></term>
      <listitem>
       <para>
-       Allows the use of the specified function and the use of any
-       operators that are implemented on top of the function.  This is
-       the only type of privilege that is applicable to functions.
-       (This syntax works for aggregate functions, as well.)
+       Allows the use of the specified function or procedure and the use of
+       any operators that are implemented on top of the function.  This is the
+       only type of privilege that is applicable to functions and procedures.
+       The <literal>FUNCTION</literal> syntax also works for aggregate
+       functions.  Or use <literal>ROUTINE</literal> to refer to a function,
+       aggregate function, or procedure regardless of what it is.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index e3e3f2ffc3..41e359548b 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -70,8 +70,8 @@
 
 REVOKE [ GRANT OPTION FOR ]
     { EXECUTE | ALL [ PRIVILEGES ] }
-    ON { FUNCTION <replaceable>function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
-         | ALL FUNCTIONS IN SCHEMA <replaceable>schema_name</replaceable> [, ...] }
+    ON { { FUNCTION | PROCEDURE | ROUTINE } <replaceable>function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
+         | ALL { FUNCTIONS | PROCEDURES | ROUTINES } IN SCHEMA <replaceable>schema_name</replaceable> [, ...] }
     FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
     [ CASCADE | RESTRICT ]
 
diff --git a/doc/src/sgml/ref/security_label.sgml b/doc/src/sgml/ref/security_label.sgml
index ce5a1c1975..6ff9e7a6b0 100644
--- a/doc/src/sgml/ref/security_label.sgml
+++ b/doc/src/sgml/ref/security_label.sgml
@@ -34,8 +34,10 @@
   LARGE OBJECT <replaceable class="parameter">large_object_oid</replaceable> |
   MATERIALIZED VIEW <replaceable class="parameter">object_name</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
+  PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   PUBLICATION <replaceable class="parameter">object_name</replaceable> |
   ROLE <replaceable class="parameter">object_name</replaceable> |
+  ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   SCHEMA <replaceable class="parameter">object_name</replaceable> |
   SEQUENCE <replaceable class="parameter">object_name</replaceable> |
   SUBSCRIPTION <replaceable class="parameter">object_name</replaceable> |
@@ -93,10 +95,12 @@ <title>Parameters</title>
     <term><replaceable class="parameter">table_name.column_name</replaceable></term>
     <term><replaceable class="parameter">aggregate_name</replaceable></term>
     <term><replaceable class="parameter">function_name</replaceable></term>
+    <term><replaceable class="parameter">procedure_name</replaceable></term>
+    <term><replaceable class="parameter">routine_name</replaceable></term>
     <listitem>
      <para>
       The name of the object to be labeled.  Names of tables,
-      aggregates, domains, foreign tables, functions, sequences, types, and
+      aggregates, domains, foreign tables, functions, procedures, routines, sequences, types, and
       views can be schema-qualified.
      </para>
     </listitem>
@@ -119,7 +123,7 @@ <title>Parameters</title>
 
     <listitem>
      <para>
-      The mode of a function or aggregate
+      The mode of a function, procedure, or aggregate
       argument: <literal>IN</literal>, <literal>OUT</literal>,
       <literal>INOUT</literal>, or <literal>VARIADIC</literal>.
       If omitted, the default is <literal>IN</literal>.
@@ -137,7 +141,7 @@ <title>Parameters</title>
 
     <listitem>
      <para>
-      The name of a function or aggregate argument.
+      The name of a function, procedure, or aggregate argument.
       Note that <command>SECURITY LABEL</command> does not actually
       pay any attention to argument names, since only the argument data
       types are needed to determine the function's identity.
@@ -150,7 +154,7 @@ <title>Parameters</title>
 
     <listitem>
      <para>
-      The data type of a function or aggregate argument.
+      The data type of a function, procedure, or aggregate argument.
      </para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 9000b3aaaa..52760ca62a 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -54,8 +54,10 @@ <title>SQL Commands</title>
    &alterOperatorClass;
    &alterOperatorFamily;
    &alterPolicy;
+   &alterProcedure;
    &alterPublication;
    &alterRole;
+   &alterRoutine;
    &alterRule;
    &alterSchema;
    &alterSequence;
@@ -76,6 +78,7 @@ <title>SQL Commands</title>
    &alterView;
    &analyze;
    &begin;
+   &call;
    &checkpoint;
    &close;
    &cluster;
@@ -103,6 +106,7 @@ <title>SQL Commands</title>
    &createOperatorClass;
    &createOperatorFamily;
    &createPolicy;
+   &createProcedure;
    &createPublication;
    &createRole;
    &createRule;
@@ -150,8 +154,10 @@ <title>SQL Commands</title>
    &dropOperatorFamily;
    &dropOwned;
    &dropPolicy;
+   &dropProcedure;
    &dropPublication;
    &dropRole;
+   &dropRoutine;
    &dropRule;
    &dropSchema;
    &dropSequence;
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 9bdb72cd98..f30767168d 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -72,6 +72,39 @@ <title>User-defined Functions</title>
   </para>
   </sect1>
 
+  <sect1 id="xproc">
+   <title>User-defined Procedures</title>
+
+  <indexterm zone="xproc">
+   <primary>procedure</primary>
+   <secondary>user-defined</secondary>
+  </indexterm>
+
+   <para>
+    A procedure is a database object similar to a function.  The difference is
+    that a procedure does not return a value, so there is no return type
+    declaration.  While a function is called as part of a query or DML
+    command, a procedure is called explicitly using
+    the <xref linkend="sql-call"> statement.
+   </para>
+
+   <para>
+    The explanations on how to define user-defined functions in the rest of
+    this chapter apply to procedures as well, except that
+    the <xref linkend="sql-createprocedure"> command is used instead, there is
+    no return type, and some other features such as strictness don't apply.
+   </para>
+
+   <para>
+    Collectively, functions and procedures are also known
+    as <firstterm>routines</firstterm><indexterm><primary>routine</primary></indexterm>.
+    There are commands such as <xref linkend="sql-alterroutine">
+    and <xref linkend="sql-droproutine"> that can operate on functions and
+    procedures without having to know which kind it is.  Note, however, that
+    there is no <literal>CREATE ROUTINE</literal> command.
+   </para>
+  </sect1>
+
   <sect1 id="xfunc-sql">
    <title>Query Language (<acronym>SQL</acronym>) Functions</title>
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ccde66a7dd..0b9d324918 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -482,6 +482,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_NAMESPACE;
 			errormsg = gettext_noop("invalid privilege type %s for schema");
 			break;
+		case ACL_OBJECT_PROCEDURE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for procedure");
+			break;
+		case ACL_OBJECT_ROUTINE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for routine");
+			break;
 		case ACL_OBJECT_TABLESPACE:
 			all_privileges = ACL_ALL_RIGHTS_TABLESPACE;
 			errormsg = gettext_noop("invalid privilege type %s for tablespace");
@@ -584,6 +592,8 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 			ExecGrant_ForeignServer(istmt);
 			break;
 		case ACL_OBJECT_FUNCTION:
+		case ACL_OBJECT_PROCEDURE:
+		case ACL_OBJECT_ROUTINE:
 			ExecGrant_Function(istmt);
 			break;
 		case ACL_OBJECT_LANGUAGE:
@@ -671,7 +681,7 @@ objectNamesToOids(GrantObjectType objtype, List *objnames)
 				ObjectWithArgs *func = (ObjectWithArgs *) lfirst(cell);
 				Oid			funcid;
 
-				funcid = LookupFuncWithArgs(func, false);
+				funcid = LookupFuncWithArgs(OBJECT_FUNCTION, func, false);
 				objects = lappend_oid(objects, funcid);
 			}
 			break;
@@ -709,6 +719,26 @@ objectNamesToOids(GrantObjectType objtype, List *objnames)
 				objects = lappend_oid(objects, oid);
 			}
 			break;
+		case ACL_OBJECT_PROCEDURE:
+			foreach(cell, objnames)
+			{
+				ObjectWithArgs *func = (ObjectWithArgs *) lfirst(cell);
+				Oid			procid;
+
+				procid = LookupFuncWithArgs(OBJECT_PROCEDURE, func, false);
+				objects = lappend_oid(objects, procid);
+			}
+			break;
+		case ACL_OBJECT_ROUTINE:
+			foreach(cell, objnames)
+			{
+				ObjectWithArgs *func = (ObjectWithArgs *) lfirst(cell);
+				Oid			routid;
+
+				routid = LookupFuncWithArgs(OBJECT_ROUTINE, func, false);
+				objects = lappend_oid(objects, routid);
+			}
+			break;
 		case ACL_OBJECT_TABLESPACE:
 			foreach(cell, objnames)
 			{
@@ -785,8 +815,10 @@ objectsInSchemaToOids(GrantObjectType objtype, List *nspnames)
 				objects = list_concat(objects, objs);
 				break;
 			case ACL_OBJECT_FUNCTION:
+			case ACL_OBJECT_PROCEDURE:
+			case ACL_OBJECT_ROUTINE:
 				{
-					ScanKeyData key[1];
+					ScanKeyData key[2];
 					Relation	rel;
 					HeapScanDesc scan;
 					HeapTuple	tuple;
@@ -795,9 +827,21 @@ objectsInSchemaToOids(GrantObjectType objtype, List *nspnames)
 								Anum_pg_proc_pronamespace,
 								BTEqualStrategyNumber, F_OIDEQ,
 								ObjectIdGetDatum(namespaceId));
+					if (objtype == ACL_OBJECT_FUNCTION)
+						ScanKeyInit(&key[1],
+									Anum_pg_proc_prorettype,
+									BTEqualStrategyNumber, F_OIDNE,
+									InvalidOid);
+					else if (objtype == ACL_OBJECT_PROCEDURE)
+						ScanKeyInit(&key[1],
+									Anum_pg_proc_prorettype,
+									BTEqualStrategyNumber, F_OIDEQ,
+									InvalidOid);
 
 					rel = heap_open(ProcedureRelationId, AccessShareLock);
-					scan = heap_beginscan_catalog(rel, 1, key);
+					scan = heap_beginscan_catalog(rel,
+												  objtype == ACL_OBJECT_ROUTINE ? 1 : 2,
+												  key);
 
 					while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
 					{
@@ -955,6 +999,14 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
 			errormsg = gettext_noop("invalid privilege type %s for function");
 			break;
+		case ACL_OBJECT_PROCEDURE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for procedure");
+			break;
+		case ACL_OBJECT_ROUTINE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for routine");
+			break;
 		case ACL_OBJECT_TYPE:
 			all_privileges = ACL_ALL_RIGHTS_TYPE;
 			errormsg = gettext_noop("invalid privilege type %s for type");
@@ -1423,7 +1475,7 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 				istmt.objtype = ACL_OBJECT_TYPE;
 				break;
 			case ProcedureRelationId:
-				istmt.objtype = ACL_OBJECT_FUNCTION;
+				istmt.objtype = ACL_OBJECT_ROUTINE;
 				break;
 			case LanguageRelationId:
 				istmt.objtype = ACL_OBJECT_LANGUAGE;
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 236f6be37e..360725d59a 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1413,7 +1413,8 @@ CREATE VIEW routines AS
            CAST(current_database() AS sql_identifier) AS routine_catalog,
            CAST(n.nspname AS sql_identifier) AS routine_schema,
            CAST(p.proname AS sql_identifier) AS routine_name,
-           CAST('FUNCTION' AS character_data) AS routine_type,
+           CAST(CASE WHEN p.prorettype <> 0 THEN 'FUNCTION' ELSE 'PROCEDURE' END
+             AS character_data) AS routine_type,
            CAST(null AS sql_identifier) AS module_catalog,
            CAST(null AS sql_identifier) AS module_schema,
            CAST(null AS sql_identifier) AS module_name,
@@ -1422,7 +1423,8 @@ CREATE VIEW routines AS
            CAST(null AS sql_identifier) AS udt_name,
 
            CAST(
-             CASE WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY'
+             CASE WHEN p.prorettype = 0 THEN NULL
+                  WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY'
                   WHEN nt.nspname = 'pg_catalog' THEN format_type(t.oid, null)
                   ELSE 'USER-DEFINED' END AS character_data)
              AS data_type,
@@ -1440,7 +1442,7 @@ CREATE VIEW routines AS
            CAST(null AS cardinal_number) AS datetime_precision,
            CAST(null AS character_data) AS interval_type,
            CAST(null AS cardinal_number) AS interval_precision,
-           CAST(current_database() AS sql_identifier) AS type_udt_catalog,
+           CAST(CASE WHEN p.prorettype <> 0 THEN current_database() END AS sql_identifier) AS type_udt_catalog,
            CAST(nt.nspname AS sql_identifier) AS type_udt_schema,
            CAST(t.typname AS sql_identifier) AS type_udt_name,
            CAST(null AS sql_identifier) AS scope_catalog,
@@ -1462,7 +1464,8 @@ CREATE VIEW routines AS
            CAST('GENERAL' AS character_data) AS parameter_style,
            CAST(CASE WHEN p.provolatile = 'i' THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_deterministic,
            CAST('MODIFIES' AS character_data) AS sql_data_access,
-           CAST(CASE WHEN p.proisstrict THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_null_call,
+           CAST(CASE WHEN p.prorettype <> 0 THEN
+             CASE WHEN p.proisstrict THEN 'YES' ELSE 'NO' END END AS yes_or_no) AS is_null_call,
            CAST(null AS character_data) AS sql_path,
            CAST('YES' AS yes_or_no) AS schema_level_routine,
            CAST(0 AS cardinal_number) AS max_dynamic_result_sets,
@@ -1503,13 +1506,15 @@ CREATE VIEW routines AS
            CAST(null AS cardinal_number) AS result_cast_maximum_cardinality,
            CAST(null AS sql_identifier) AS result_cast_dtd_identifier
 
-    FROM pg_namespace n, pg_proc p, pg_language l,
-         pg_type t, pg_namespace nt
+    FROM (pg_namespace n
+          JOIN pg_proc p ON n.oid = p.pronamespace
+          JOIN pg_language l ON p.prolang = l.oid)
+         LEFT JOIN
+         (pg_type t JOIN pg_namespace nt ON t.typnamespace = nt.oid)
+         ON p.prorettype = t.oid
 
-    WHERE n.oid = p.pronamespace AND p.prolang = l.oid
-          AND p.prorettype = t.oid AND t.typnamespace = nt.oid
-          AND (pg_has_role(p.proowner, 'USAGE')
-               OR has_function_privilege(p.oid, 'EXECUTE'));
+    WHERE (pg_has_role(p.proowner, 'USAGE')
+           OR has_function_privilege(p.oid, 'EXECUTE'));
 
 GRANT SELECT ON routines TO PUBLIC;
 
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 8d55c76fc4..9553675975 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -566,6 +566,9 @@ static const struct object_type_map
 	{
 		"function", OBJECT_FUNCTION
 	},
+	{
+		"procedure", OBJECT_PROCEDURE
+	},
 	/* OCLASS_TYPE */
 	{
 		"type", OBJECT_TYPE
@@ -884,13 +887,11 @@ get_object_address(ObjectType objtype, Node *object,
 				address = get_object_address_type(objtype, castNode(TypeName, object), missing_ok);
 				break;
 			case OBJECT_AGGREGATE:
-				address.classId = ProcedureRelationId;
-				address.objectId = LookupAggWithArgs(castNode(ObjectWithArgs, object), missing_ok);
-				address.objectSubId = 0;
-				break;
 			case OBJECT_FUNCTION:
+			case OBJECT_PROCEDURE:
+			case OBJECT_ROUTINE:
 				address.classId = ProcedureRelationId;
-				address.objectId = LookupFuncWithArgs(castNode(ObjectWithArgs, object), missing_ok);
+				address.objectId = LookupFuncWithArgs(objtype, castNode(ObjectWithArgs, object), missing_ok);
 				address.objectSubId = 0;
 				break;
 			case OBJECT_OPERATOR:
@@ -2025,6 +2026,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 	 */
 	if (type == OBJECT_AGGREGATE ||
 		type == OBJECT_FUNCTION ||
+		type == OBJECT_PROCEDURE ||
+		type == OBJECT_ROUTINE ||
 		type == OBJECT_OPERATOR ||
 		type == OBJECT_CAST ||
 		type == OBJECT_AMOP ||
@@ -2168,6 +2171,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_AGGREGATE:
 		case OBJECT_OPERATOR:
 			{
@@ -2253,6 +2258,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AGGREGATE:
 		case OBJECT_FUNCTION:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 			if (!pg_proc_ownercheck(address.objectId, roleid))
 				aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_PROC,
 							   NameListToString((castNode(ObjectWithArgs, object))->objname));
@@ -4026,6 +4033,8 @@ getProcedureTypeDescription(StringInfo buffer, Oid procid)
 
 	if (procForm->proisagg)
 		appendStringInfoString(buffer, "aggregate");
+	else if (procForm->prorettype == InvalidOid)
+		appendStringInfoString(buffer, "procedure");
 	else
 		appendStringInfoString(buffer, "function");
 
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 47916cfb54..7d05e4bdb2 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -857,7 +857,8 @@ fmgr_sql_validator(PG_FUNCTION_ARGS)
 
 	/* Disallow pseudotype result */
 	/* except for RECORD, VOID, or polymorphic */
-	if (get_typtype(proc->prorettype) == TYPTYPE_PSEUDO &&
+	if (proc->prorettype &&
+		get_typtype(proc->prorettype) == TYPTYPE_PSEUDO &&
 		proc->prorettype != RECORDOID &&
 		proc->prorettype != VOIDOID &&
 		!IsPolymorphicType(proc->prorettype))
diff --git a/src/backend/commands/aggregatecmds.c b/src/backend/commands/aggregatecmds.c
index adc9877e79..2e2ee883e2 100644
--- a/src/backend/commands/aggregatecmds.c
+++ b/src/backend/commands/aggregatecmds.c
@@ -307,7 +307,7 @@ DefineAggregate(ParseState *pstate, List *name, List *args, bool oldstyle, List
 		interpret_function_parameter_list(pstate,
 										  args,
 										  InvalidOid,
-										  true, /* is an aggregate */
+										  OBJECT_AGGREGATE,
 										  &parameterTypes,
 										  &allParameterTypes,
 										  &parameterModes,
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 4f8147907c..21e3f1efe1 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -378,6 +378,8 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
 		case OBJECT_LANGUAGE:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TSCONFIGURATION:
 		case OBJECT_TSDICTIONARY:
@@ -495,6 +497,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_OPERATOR:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TSCONFIGURATION:
 		case OBJECT_TSDICTIONARY:
@@ -842,6 +846,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_OPERATOR:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TABLESPACE:
 		case OBJECT_TSDICTIONARY:
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 2b30677d6f..7e6baa1928 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -26,6 +26,7 @@
 #include "nodes/makefuncs.h"
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 
@@ -91,21 +92,12 @@ RemoveObjects(DropStmt *stmt)
 		 */
 		if (stmt->removeType == OBJECT_FUNCTION)
 		{
-			Oid			funcOid = address.objectId;
-			HeapTuple	tup;
-
-			tup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcOid));
-			if (!HeapTupleIsValid(tup)) /* should not happen */
-				elog(ERROR, "cache lookup failed for function %u", funcOid);
-
-			if (((Form_pg_proc) GETSTRUCT(tup))->proisagg)
+			if (get_func_isagg(address.objectId))
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 						 errmsg("\"%s\" is an aggregate function",
 								NameListToString(castNode(ObjectWithArgs, object)->objname)),
 						 errhint("Use DROP AGGREGATE to drop aggregate functions.")));
-
-			ReleaseSysCache(tup);
 		}
 
 		/* Check permissions. */
@@ -338,6 +330,32 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				}
 				break;
 			}
+		case OBJECT_PROCEDURE:
+			{
+				ObjectWithArgs *owa = castNode(ObjectWithArgs, object);
+
+				if (!schema_does_not_exist_skipping(owa->objname, &msg, &name) &&
+					!type_in_list_does_not_exist_skipping(owa->objargs, &msg, &name))
+				{
+					msg = gettext_noop("procedure %s(%s) does not exist, skipping");
+					name = NameListToString(owa->objname);
+					args = TypeNameListToString(owa->objargs);
+				}
+				break;
+			}
+		case OBJECT_ROUTINE:
+			{
+				ObjectWithArgs *owa = castNode(ObjectWithArgs, object);
+
+				if (!schema_does_not_exist_skipping(owa->objname, &msg, &name) &&
+					!type_in_list_does_not_exist_skipping(owa->objargs, &msg, &name))
+				{
+					msg = gettext_noop("routine %s(%s) does not exist, skipping");
+					name = NameListToString(owa->objname);
+					args = TypeNameListToString(owa->objargs);
+				}
+				break;
+			}
 		case OBJECT_AGGREGATE:
 			{
 				ObjectWithArgs *owa = castNode(ObjectWithArgs, object);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index fa7d0d015a..a602c20b41 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -106,8 +106,10 @@ static event_trigger_support_data event_trigger_support[] = {
 	{"OPERATOR CLASS", true},
 	{"OPERATOR FAMILY", true},
 	{"POLICY", true},
+	{"PROCEDURE", true},
 	{"PUBLICATION", true},
 	{"ROLE", false},
+	{"ROUTINE", true},
 	{"RULE", true},
 	{"SCHEMA", true},
 	{"SEQUENCE", true},
@@ -1103,8 +1105,10 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_OPERATOR:
 		case OBJECT_OPFAMILY:
 		case OBJECT_POLICY:
+		case OBJECT_PROCEDURE:
 		case OBJECT_PUBLICATION:
 		case OBJECT_PUBLICATION_REL:
+		case OBJECT_ROUTINE:
 		case OBJECT_RULE:
 		case OBJECT_SCHEMA:
 		case OBJECT_SEQUENCE:
@@ -1215,6 +1219,8 @@ EventTriggerSupportsGrantObjectType(GrantObjectType objtype)
 		case ACL_OBJECT_LANGUAGE:
 		case ACL_OBJECT_LARGEOBJECT:
 		case ACL_OBJECT_NAMESPACE:
+		case ACL_OBJECT_PROCEDURE:
+		case ACL_OBJECT_ROUTINE:
 		case ACL_OBJECT_TYPE:
 			return true;
 
@@ -2243,6 +2249,10 @@ stringify_grantobjtype(GrantObjectType objtype)
 			return "LARGE OBJECT";
 		case ACL_OBJECT_NAMESPACE:
 			return "SCHEMA";
+		case ACL_OBJECT_PROCEDURE:
+			return "PROCEDURE";
+		case ACL_OBJECT_ROUTINE:
+			return "ROUTINE";
 		case ACL_OBJECT_TABLESPACE:
 			return "TABLESPACE";
 		case ACL_OBJECT_TYPE:
@@ -2285,6 +2295,10 @@ stringify_adefprivs_objtype(GrantObjectType objtype)
 			return "LARGE OBJECTS";
 		case ACL_OBJECT_NAMESPACE:
 			return "SCHEMAS";
+		case ACL_OBJECT_PROCEDURE:
+			return "PROCEDURES";
+		case ACL_OBJECT_ROUTINE:
+			return "ROUTINES";
 		case ACL_OBJECT_TABLESPACE:
 			return "TABLESPACES";
 		case ACL_OBJECT_TYPE:
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index 7de844b2ca..1f3156d870 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -51,6 +51,8 @@
 #include "commands/alter.h"
 #include "commands/defrem.h"
 #include "commands/proclang.h"
+#include "executor/execdesc.h"
+#include "executor/executor.h"
 #include "miscadmin.h"
 #include "optimizer/var.h"
 #include "parser/parse_coerce.h"
@@ -179,7 +181,7 @@ void
 interpret_function_parameter_list(ParseState *pstate,
 								  List *parameters,
 								  Oid languageOid,
-								  bool is_aggregate,
+								  ObjectType objtype,
 								  oidvector **parameterTypes,
 								  ArrayType **allParameterTypes,
 								  ArrayType **parameterModes,
@@ -233,7 +235,7 @@ interpret_function_parameter_list(ParseState *pstate,
 							 errmsg("SQL function cannot accept shell type %s",
 									TypeNameToString(t))));
 				/* We don't allow creating aggregates on shell types either */
-				else if (is_aggregate)
+				else if (objtype == OBJECT_AGGREGATE)
 					ereport(ERROR,
 							(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 							 errmsg("aggregate cannot accept shell type %s",
@@ -262,16 +264,28 @@ interpret_function_parameter_list(ParseState *pstate,
 
 		if (t->setof)
 		{
-			if (is_aggregate)
+			if (objtype == OBJECT_AGGREGATE)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 						 errmsg("aggregates cannot accept set arguments")));
+			else if (objtype == OBJECT_PROCEDURE)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+						 errmsg("procedures cannot accept set arguments")));
 			else
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 						 errmsg("functions cannot accept set arguments")));
 		}
 
+		if (objtype == OBJECT_PROCEDURE)
+		{
+			if (fp->mode == FUNC_PARAM_OUT || fp->mode == FUNC_PARAM_INOUT)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 (errmsg("procedures cannot have OUT parameters"))));
+		}
+
 		/* handle input parameters */
 		if (fp->mode != FUNC_PARAM_OUT && fp->mode != FUNC_PARAM_TABLE)
 		{
@@ -990,7 +1004,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 	interpret_function_parameter_list(pstate,
 									  stmt->parameters,
 									  languageOid,
-									  false,	/* not an aggregate */
+									  stmt->is_procedure ? OBJECT_PROCEDURE : OBJECT_FUNCTION,
 									  &parameterTypes,
 									  &allParameterTypes,
 									  &parameterModes,
@@ -999,7 +1013,24 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 									  &variadicArgType,
 									  &requiredResultType);
 
-	if (stmt->returnType)
+	if (stmt->is_procedure)
+	{
+		Assert(!stmt->returnType);
+
+		prorettype = InvalidOid;
+		returnsSet = false;
+
+		if (isStrict)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+					 errmsg("procedure cannot be declared as strict")));
+
+		if (isWindowFunc)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+					 errmsg("procedure cannot have WINDOW attribute")));
+	}
+	else if (stmt->returnType)
 	{
 		/* explicit RETURNS clause */
 		compute_return_type(stmt->returnType, languageOid,
@@ -1182,7 +1213,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 
 	rel = heap_open(ProcedureRelationId, RowExclusiveLock);
 
-	funcOid = LookupFuncWithArgs(stmt->func, false);
+	funcOid = LookupFuncWithArgs(stmt->objtype, stmt->func, false);
 
 	tup = SearchSysCacheCopy1(PROCOID, ObjectIdGetDatum(funcOid));
 	if (!HeapTupleIsValid(tup)) /* should not happen */
@@ -1219,6 +1250,11 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 			elog(ERROR, "option \"%s\" not recognized", defel->defname);
 	}
 
+	if (procForm->prorettype == InvalidOid && strict_item)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+				 errmsg("procedure strictness cannot be changed")));
+
 	if (volatility_item)
 		procForm->provolatile = interpret_func_volatility(volatility_item);
 	if (strict_item)
@@ -1472,7 +1508,7 @@ CreateCast(CreateCastStmt *stmt)
 	{
 		Form_pg_proc procstruct;
 
-		funcid = LookupFuncWithArgs(stmt->func, false);
+		funcid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->func, false);
 
 		tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
 		if (!HeapTupleIsValid(tuple))
@@ -1853,7 +1889,7 @@ CreateTransform(CreateTransformStmt *stmt)
 	 */
 	if (stmt->fromsql)
 	{
-		fromsqlfuncid = LookupFuncWithArgs(stmt->fromsql, false);
+		fromsqlfuncid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->fromsql, false);
 
 		if (!pg_proc_ownercheck(fromsqlfuncid, GetUserId()))
 			aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_PROC, NameListToString(stmt->fromsql->objname));
@@ -1879,7 +1915,7 @@ CreateTransform(CreateTransformStmt *stmt)
 
 	if (stmt->tosql)
 	{
-		tosqlfuncid = LookupFuncWithArgs(stmt->tosql, false);
+		tosqlfuncid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->tosql, false);
 
 		if (!pg_proc_ownercheck(tosqlfuncid, GetUserId()))
 			aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_PROC, NameListToString(stmt->tosql->objname));
@@ -2168,3 +2204,80 @@ ExecuteDoStmt(DoStmt *stmt)
 	/* execute the inline handler */
 	OidFunctionCall1(laninline, PointerGetDatum(codeblock));
 }
+
+/*
+ * Execute CALL statement
+ */
+void
+ExecuteCallStmt(ParseState *pstate, CallStmt *stmt)
+{
+	List	   *targs;
+	ListCell   *lc;
+	Node	   *node;
+	FuncExpr   *fexpr;
+	int			nargs;
+	int			i;
+	AclResult   aclresult;
+	FmgrInfo	flinfo;
+	FunctionCallInfoData fcinfo;
+
+	targs = NIL;
+	foreach(lc, stmt->funccall->args)
+	{
+		targs = lappend(targs, transformExpr(pstate,
+											 (Node *) lfirst(lc),
+											 EXPR_KIND_CALL));
+	}
+
+	node = ParseFuncOrColumn(pstate,
+							 stmt->funccall->funcname,
+							 targs,
+							 pstate->p_last_srf,
+							 stmt->funccall,
+							 true,
+							 stmt->funccall->location);
+
+	fexpr = castNode(FuncExpr, node);
+
+	aclresult = pg_proc_aclcheck(fexpr->funcid, GetUserId(), ACL_EXECUTE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, ACL_KIND_PROC, get_func_name(fexpr->funcid));
+	InvokeFunctionExecuteHook(fexpr->funcid);
+
+	nargs = list_length(fexpr->args);
+
+	/* safety check; see ExecInitFunc() */
+	if (nargs > FUNC_MAX_ARGS)
+		ereport(ERROR,
+				(errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+				 errmsg_plural("cannot pass more than %d argument to a procedure",
+							   "cannot pass more than %d arguments to a procedure",
+							   FUNC_MAX_ARGS,
+							   FUNC_MAX_ARGS)));
+
+	fmgr_info(fexpr->funcid, &flinfo);
+	InitFunctionCallInfoData(fcinfo, &flinfo, nargs, fexpr->inputcollid, NULL, NULL);
+
+	i = 0;
+	foreach (lc, fexpr->args)
+	{
+		EState	   *estate;
+		ExprState  *exprstate;
+		ExprContext *econtext;
+		Datum		val;
+		bool		isnull;
+
+		estate = CreateExecutorState();
+		exprstate = ExecPrepareExpr(lfirst(lc), estate);
+		econtext = CreateStandaloneExprContext();
+		val = ExecEvalExprSwitchContext(exprstate, econtext, &isnull);
+		FreeExecutorState(estate);
+
+		fcinfo.arg[i] = val;
+		fcinfo.argnull[i] = isnull;
+
+		i++;
+	}
+
+	FunctionCallInvoke(&fcinfo);
+}
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 1641e68abe..35c7c67bf5 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -520,7 +520,7 @@ DefineOpClass(CreateOpClassStmt *stmt)
 							 errmsg("invalid procedure number %d,"
 									" must be between 1 and %d",
 									item->number, maxProcNumber)));
-				funcOid = LookupFuncWithArgs(item->name, false);
+				funcOid = LookupFuncWithArgs(OBJECT_FUNCTION, item->name, false);
 #ifdef NOT_USED
 				/* XXX this is unnecessary given the superuser check above */
 				/* Caller must own function */
@@ -894,7 +894,7 @@ AlterOpFamilyAdd(AlterOpFamilyStmt *stmt, Oid amoid, Oid opfamilyoid,
 							 errmsg("invalid procedure number %d,"
 									" must be between 1 and %d",
 									item->number, maxProcNumber)));
-				funcOid = LookupFuncWithArgs(item->name, false);
+				funcOid = LookupFuncWithArgs(OBJECT_FUNCTION, item->name, false);
 #ifdef NOT_USED
 				/* XXX this is unnecessary given the superuser check above */
 				/* Caller must own function */
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 98eb777421..564ccb004f 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -390,6 +390,7 @@ sql_fn_post_column_ref(ParseState *pstate, ColumnRef *cref, Node *var)
 								  list_make1(param),
 								  pstate->p_last_srf,
 								  NULL,
+								  false,
 								  cref->location);
 	}
 
@@ -658,7 +659,8 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK)
 	fcache->rettype = rettype;
 
 	/* Fetch the typlen and byval info for the result type */
-	get_typlenbyval(rettype, &fcache->typlen, &fcache->typbyval);
+	if (rettype)
+		get_typlenbyval(rettype, &fcache->typlen, &fcache->typbyval);
 
 	/* Remember whether we're returning setof something */
 	fcache->returnsSet = procedureStruct->proretset;
@@ -1322,7 +1324,7 @@ fmgr_sql(PG_FUNCTION_ARGS)
 		else
 		{
 			/* Should only get here for VOID functions */
-			Assert(fcache->rettype == VOIDOID);
+			Assert(fcache->rettype == InvalidOid || fcache->rettype == VOIDOID);
 			fcinfo->isnull = true;
 			result = (Datum) 0;
 		}
@@ -1546,7 +1548,10 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
 	if (modifyTargetList)
 		*modifyTargetList = false;	/* initialize for no change */
 	if (junkFilter)
-		*junkFilter = NULL;		/* initialize in case of VOID result */
+		*junkFilter = NULL;		/* initialize in case of procedure/VOID result */
+
+	if (!rettype)
+		return false;
 
 	/*
 	 * Find the last canSetTag query in the list.  This isn't necessarily the
@@ -1591,7 +1596,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
 	else
 	{
 		/* Empty function body, or last statement is a utility command */
-		if (rettype != VOIDOID)
+		if (rettype && rettype != VOIDOID)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 					 errmsg("return type mismatch in function declared to return %s",
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 76e75459b4..751f7e7eb2 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3208,6 +3208,16 @@ _copyClosePortalStmt(const ClosePortalStmt *from)
 	return newnode;
 }
 
+static CallStmt *
+_copyCallStmt(const CallStmt *from)
+{
+	CallStmt *newnode = makeNode(CallStmt);
+
+	COPY_NODE_FIELD(funccall);
+
+	return newnode;
+}
+
 static ClusterStmt *
 _copyClusterStmt(const ClusterStmt *from)
 {
@@ -3409,6 +3419,7 @@ _copyCreateFunctionStmt(const CreateFunctionStmt *from)
 	COPY_NODE_FIELD(funcname);
 	COPY_NODE_FIELD(parameters);
 	COPY_NODE_FIELD(returnType);
+	COPY_SCALAR_FIELD(is_procedure);
 	COPY_NODE_FIELD(options);
 	COPY_NODE_FIELD(withClause);
 
@@ -3433,6 +3444,7 @@ _copyAlterFunctionStmt(const AlterFunctionStmt *from)
 {
 	AlterFunctionStmt *newnode = makeNode(AlterFunctionStmt);
 
+	COPY_SCALAR_FIELD(objtype);
 	COPY_NODE_FIELD(func);
 	COPY_NODE_FIELD(actions);
 
@@ -5102,6 +5114,9 @@ copyObjectImpl(const void *from)
 		case T_ClosePortalStmt:
 			retval = _copyClosePortalStmt(from);
 			break;
+		case T_CallStmt:
+			retval = _copyCallStmt(from);
+			break;
 		case T_ClusterStmt:
 			retval = _copyClusterStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 2866fd7b4a..2e869a9d5d 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1201,6 +1201,14 @@ _equalClosePortalStmt(const ClosePortalStmt *a, const ClosePortalStmt *b)
 	return true;
 }
 
+static bool
+_equalCallStmt(const CallStmt *a, const CallStmt *b)
+{
+	COMPARE_NODE_FIELD(funccall);
+
+	return true;
+}
+
 static bool
 _equalClusterStmt(const ClusterStmt *a, const ClusterStmt *b)
 {
@@ -1364,6 +1372,7 @@ _equalCreateFunctionStmt(const CreateFunctionStmt *a, const CreateFunctionStmt *
 	COMPARE_NODE_FIELD(funcname);
 	COMPARE_NODE_FIELD(parameters);
 	COMPARE_NODE_FIELD(returnType);
+	COMPARE_SCALAR_FIELD(is_procedure);
 	COMPARE_NODE_FIELD(options);
 	COMPARE_NODE_FIELD(withClause);
 
@@ -1384,6 +1393,7 @@ _equalFunctionParameter(const FunctionParameter *a, const FunctionParameter *b)
 static bool
 _equalAlterFunctionStmt(const AlterFunctionStmt *a, const AlterFunctionStmt *b)
 {
+	COMPARE_SCALAR_FIELD(objtype);
 	COMPARE_NODE_FIELD(func);
 	COMPARE_NODE_FIELD(actions);
 
@@ -3246,6 +3256,9 @@ equal(const void *a, const void *b)
 		case T_ClosePortalStmt:
 			retval = _equalClosePortalStmt(a, b);
 			break;
+		case T_CallStmt:
+			retval = _equalCallStmt(a, b);
+			break;
 		case T_ClusterStmt:
 			retval = _equalClusterStmt(a, b);
 			break;
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 66e098f488..52140dabc1 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4379,6 +4379,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 	if (funcform->prolang != SQLlanguageId ||
 		funcform->prosecdef ||
 		funcform->proretset ||
+		funcform->prorettype == InvalidOid ||
 		funcform->prorettype == RECORDOID ||
 		!heap_attisnull(func_tuple, Anum_pg_proc_proconfig) ||
 		funcform->pronargs != list_length(args))
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c301ca465d..ebfc94f896 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -253,7 +253,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		AlterCompositeTypeStmt AlterUserMappingStmt
 		AlterRoleStmt AlterRoleSetStmt AlterPolicyStmt
 		AlterDefaultPrivilegesStmt DefACLAction
-		AnalyzeStmt ClosePortalStmt ClusterStmt CommentStmt
+		AnalyzeStmt CallStmt ClosePortalStmt ClusterStmt CommentStmt
 		ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt
 		CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
@@ -611,7 +611,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	BACKWARD BEFORE BEGIN_P BETWEEN BIGINT BINARY BIT
 	BOOLEAN_P BOTH BY
 
-	CACHE CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
+	CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
 	CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
 	CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
 	COMMITTED CONCURRENTLY CONFIGURATION CONFLICT CONNECTION CONSTRAINT
@@ -660,14 +660,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
-	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROGRAM PUBLICATION
+	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
 	QUOTE
 
 	RANGE READ REAL REASSIGN RECHECK RECURSIVE REF REFERENCES REFERENCING
 	REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA
 	RESET RESTART RESTRICT RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP
-	ROW ROWS RULE
+	ROUTINE ROUTINES ROW ROWS RULE
 
 	SAVEPOINT SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
 	SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
@@ -845,6 +845,7 @@ stmt :
 			| AlterTSDictionaryStmt
 			| AlterUserMappingStmt
 			| AnalyzeStmt
+			| CallStmt
 			| CheckPointStmt
 			| ClosePortalStmt
 			| ClusterStmt
@@ -940,6 +941,20 @@ stmt :
 				{ $$ = NULL; }
 		;
 
+/*****************************************************************************
+ *
+ * CALL statement
+ *
+ *****************************************************************************/
+
+CallStmt:	CALL func_application
+				{
+					CallStmt *n = makeNode(CallStmt);
+					n->funccall = castNode(FuncCall, $2);
+					$$ = (Node *)n;
+				}
+		;
+
 /*****************************************************************************
  *
  * Create a new Postgres DBMS role
@@ -4554,6 +4569,24 @@ AlterExtensionContentsStmt:
 					n->object = (Node *) lcons(makeString($9), $7);
 					$$ = (Node *)n;
 				}
+			| ALTER EXTENSION name add_drop PROCEDURE function_with_argtypes
+				{
+					AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
+					n->extname = $3;
+					n->action = $4;
+					n->objtype = OBJECT_PROCEDURE;
+					n->object = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			| ALTER EXTENSION name add_drop ROUTINE function_with_argtypes
+				{
+					AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
+					n->extname = $3;
+					n->action = $4;
+					n->objtype = OBJECT_ROUTINE;
+					n->object = (Node *) $6;
+					$$ = (Node *)n;
+				}
 			| ALTER EXTENSION name add_drop SCHEMA name
 				{
 					AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
@@ -6436,6 +6469,22 @@ CommentStmt:
 					n->comment = $8;
 					$$ = (Node *) n;
 				}
+			| COMMENT ON PROCEDURE function_with_argtypes IS comment_text
+				{
+					CommentStmt *n = makeNode(CommentStmt);
+					n->objtype = OBJECT_PROCEDURE;
+					n->object = (Node *) $4;
+					n->comment = $6;
+					$$ = (Node *) n;
+				}
+			| COMMENT ON ROUTINE function_with_argtypes IS comment_text
+				{
+					CommentStmt *n = makeNode(CommentStmt);
+					n->objtype = OBJECT_ROUTINE;
+					n->object = (Node *) $4;
+					n->comment = $6;
+					$$ = (Node *) n;
+				}
 			| COMMENT ON RULE name ON any_name IS comment_text
 				{
 					CommentStmt *n = makeNode(CommentStmt);
@@ -6614,6 +6663,26 @@ SecLabelStmt:
 					n->label = $9;
 					$$ = (Node *) n;
 				}
+			| SECURITY LABEL opt_provider ON PROCEDURE function_with_argtypes
+			  IS security_label
+				{
+					SecLabelStmt *n = makeNode(SecLabelStmt);
+					n->provider = $3;
+					n->objtype = OBJECT_PROCEDURE;
+					n->object = (Node *) $6;
+					n->label = $8;
+					$$ = (Node *) n;
+				}
+			| SECURITY LABEL opt_provider ON ROUTINE function_with_argtypes
+			  IS security_label
+				{
+					SecLabelStmt *n = makeNode(SecLabelStmt);
+					n->provider = $3;
+					n->objtype = OBJECT_ROUTINE;
+					n->object = (Node *) $6;
+					n->label = $8;
+					$$ = (Node *) n;
+				}
 		;
 
 opt_provider:	FOR NonReservedWord_or_Sconst	{ $$ = $2; }
@@ -6977,6 +7046,22 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| PROCEDURE function_with_argtypes_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = ACL_OBJECT_PROCEDURE;
+					n->objs = $2;
+					$$ = n;
+				}
+			| ROUTINE function_with_argtypes_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = ACL_OBJECT_ROUTINE;
+					n->objs = $2;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -7057,6 +7142,22 @@ privilege_target:
 					n->objs = $5;
 					$$ = n;
 				}
+			| ALL PROCEDURES IN_P SCHEMA name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_ALL_IN_SCHEMA;
+					n->objtype = ACL_OBJECT_PROCEDURE;
+					n->objs = $5;
+					$$ = n;
+				}
+			| ALL ROUTINES IN_P SCHEMA name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_ALL_IN_SCHEMA;
+					n->objtype = ACL_OBJECT_ROUTINE;
+					n->objs = $5;
+					$$ = n;
+				}
 		;
 
 
@@ -7213,6 +7314,7 @@ DefACLAction:
 defacl_privilege_target:
 			TABLES			{ $$ = ACL_OBJECT_RELATION; }
 			| FUNCTIONS		{ $$ = ACL_OBJECT_FUNCTION; }
+			| ROUTINES		{ $$ = ACL_OBJECT_FUNCTION; }
 			| SEQUENCES		{ $$ = ACL_OBJECT_SEQUENCE; }
 			| TYPES_P		{ $$ = ACL_OBJECT_TYPE; }
 			| SCHEMAS		{ $$ = ACL_OBJECT_NAMESPACE; }
@@ -7413,6 +7515,18 @@ CreateFunctionStmt:
 					n->withClause = $7;
 					$$ = (Node *)n;
 				}
+			| CREATE opt_or_replace PROCEDURE func_name func_args_with_defaults
+			  createfunc_opt_list
+				{
+					CreateFunctionStmt *n = makeNode(CreateFunctionStmt);
+					n->replace = $2;
+					n->funcname = $4;
+					n->parameters = $5;
+					n->returnType = NULL;
+					n->is_procedure = true;
+					n->options = $6;
+					$$ = (Node *)n;
+				}
 		;
 
 opt_or_replace:
@@ -7830,7 +7944,7 @@ table_func_column_list:
 		;
 
 /*****************************************************************************
- * ALTER FUNCTION
+ * ALTER FUNCTION / ALTER PROCEDURE / ALTER ROUTINE
  *
  * RENAME and OWNER subcommands are already provided by the generic
  * ALTER infrastructure, here we just specify alterations that can
@@ -7841,6 +7955,23 @@ AlterFunctionStmt:
 			ALTER FUNCTION function_with_argtypes alterfunc_opt_list opt_restrict
 				{
 					AlterFunctionStmt *n = makeNode(AlterFunctionStmt);
+					n->objtype = OBJECT_FUNCTION;
+					n->func = $3;
+					n->actions = $4;
+					$$ = (Node *) n;
+				}
+			| ALTER PROCEDURE function_with_argtypes alterfunc_opt_list opt_restrict
+				{
+					AlterFunctionStmt *n = makeNode(AlterFunctionStmt);
+					n->objtype = OBJECT_PROCEDURE;
+					n->func = $3;
+					n->actions = $4;
+					$$ = (Node *) n;
+				}
+			| ALTER ROUTINE function_with_argtypes alterfunc_opt_list opt_restrict
+				{
+					AlterFunctionStmt *n = makeNode(AlterFunctionStmt);
+					n->objtype = OBJECT_ROUTINE;
 					n->func = $3;
 					n->actions = $4;
 					$$ = (Node *) n;
@@ -7865,6 +7996,8 @@ opt_restrict:
  *		QUERY:
  *
  *		DROP FUNCTION funcname (arg1, arg2, ...) [ RESTRICT | CASCADE ]
+ *		DROP PROCEDURE procname (arg1, arg2, ...) [ RESTRICT | CASCADE ]
+ *		DROP ROUTINE routname (arg1, arg2, ...) [ RESTRICT | CASCADE ]
  *		DROP AGGREGATE aggname (arg1, ...) [ RESTRICT | CASCADE ]
  *		DROP OPERATOR opname (leftoperand_typ, rightoperand_typ) [ RESTRICT | CASCADE ]
  *
@@ -7891,6 +8024,46 @@ RemoveFuncStmt:
 					n->concurrent = false;
 					$$ = (Node *)n;
 				}
+			| DROP PROCEDURE function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_PROCEDURE;
+					n->objects = $3;
+					n->behavior = $4;
+					n->missing_ok = false;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
+			| DROP PROCEDURE IF_P EXISTS function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_PROCEDURE;
+					n->objects = $5;
+					n->behavior = $6;
+					n->missing_ok = true;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
+			| DROP ROUTINE function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_ROUTINE;
+					n->objects = $3;
+					n->behavior = $4;
+					n->missing_ok = false;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
+			| DROP ROUTINE IF_P EXISTS function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_ROUTINE;
+					n->objects = $5;
+					n->behavior = $6;
+					n->missing_ok = true;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
 		;
 
 RemoveAggrStmt:
@@ -8348,6 +8521,15 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = true;
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -8357,6 +8539,15 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ROUTINE function_with_argtypes RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER SCHEMA name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -8736,6 +8927,22 @@ AlterObjectDependsStmt:
 					n->extname = makeString($7);
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes DEPENDS ON EXTENSION name
+				{
+					AlterObjectDependsStmt *n = makeNode(AlterObjectDependsStmt);
+					n->objectType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->extname = makeString($7);
+					$$ = (Node *)n;
+				}
+			| ALTER ROUTINE function_with_argtypes DEPENDS ON EXTENSION name
+				{
+					AlterObjectDependsStmt *n = makeNode(AlterObjectDependsStmt);
+					n->objectType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->extname = makeString($7);
+					$$ = (Node *)n;
+				}
 			| ALTER TRIGGER name ON qualified_name DEPENDS ON EXTENSION name
 				{
 					AlterObjectDependsStmt *n = makeNode(AlterObjectDependsStmt);
@@ -8851,6 +9058,24 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER ROUTINE function_with_argtypes SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER TABLE relation_expr SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -9126,6 +9351,22 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $9;
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
+			| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
 			| ALTER SCHEMA name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -14689,6 +14930,7 @@ unreserved_keyword:
 			| BEGIN_P
 			| BY
 			| CACHE
+			| CALL
 			| CALLED
 			| CASCADE
 			| CASCADED
@@ -14848,6 +15090,7 @@ unreserved_keyword:
 			| PRIVILEGES
 			| PROCEDURAL
 			| PROCEDURE
+			| PROCEDURES
 			| PROGRAM
 			| PUBLICATION
 			| QUOTE
@@ -14874,6 +15117,8 @@ unreserved_keyword:
 			| ROLE
 			| ROLLBACK
 			| ROLLUP
+			| ROUTINE
+			| ROUTINES
 			| ROWS
 			| RULE
 			| SAVEPOINT
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 64111f315e..4c4f4cdc3d 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -508,6 +508,14 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 
 			break;
 
+		case EXPR_KIND_CALL:
+			if (isAgg)
+				err = _("aggregate functions are not allowed in CALL arguments");
+			else
+				err = _("grouping operations are not allowed in CALL arguments");
+
+			break;
+
 			/*
 			 * There is intentionally no default: case here, so that the
 			 * compiler will warn if we add a new ParseExprKind without
@@ -883,6 +891,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_PARTITION_EXPRESSION:
 			err = _("window functions are not allowed in partition key expression");
 			break;
+		case EXPR_KIND_CALL:
+			err = _("window functions are not allowed in CALL arguments");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 86d1da0677..29f9da796f 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -480,6 +480,7 @@ transformIndirection(ParseState *pstate, A_Indirection *ind)
 										  list_make1(result),
 										  last_srf,
 										  NULL,
+										  false,
 										  location);
 			if (newresult == NULL)
 				unknown_attribute(pstate, result, strVal(n), location);
@@ -629,6 +630,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 											 list_make1(node),
 											 pstate->p_last_srf,
 											 NULL,
+											 false,
 											 cref->location);
 				}
 				break;
@@ -676,6 +678,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 											 list_make1(node),
 											 pstate->p_last_srf,
 											 NULL,
+											 false,
 											 cref->location);
 				}
 				break;
@@ -736,6 +739,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 											 list_make1(node),
 											 pstate->p_last_srf,
 											 NULL,
+											 false,
 											 cref->location);
 				}
 				break;
@@ -1477,6 +1481,7 @@ transformFuncCall(ParseState *pstate, FuncCall *fn)
 							 targs,
 							 last_srf,
 							 fn,
+							 false,
 							 fn->location);
 }
 
@@ -1812,6 +1817,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
+		case EXPR_KIND_CALL:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -3462,6 +3468,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "WHEN";
 		case EXPR_KIND_PARTITION_EXPRESSION:
 			return "PARTITION BY";
+		case EXPR_KIND_CALL:
+			return "CALL";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index a11843332b..2f20516e76 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -71,7 +71,7 @@ static Node *ParseComplexProjection(ParseState *pstate, const char *funcname,
  */
 Node *
 ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
-				  Node *last_srf, FuncCall *fn, int location)
+				  Node *last_srf, FuncCall *fn, bool proc_call, int location)
 {
 	bool		is_column = (fn == NULL);
 	List	   *agg_order = (fn ? fn->agg_order : NIL);
@@ -263,7 +263,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 						   actual_arg_types[0], rettype, -1,
 						   COERCION_EXPLICIT, COERCE_EXPLICIT_CALL, location);
 	}
-	else if (fdresult == FUNCDETAIL_NORMAL)
+	else if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
 	{
 		/*
 		 * Normal function found; was there anything indicating it must be an
@@ -306,6 +306,26 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 					 errmsg("OVER specified, but %s is not a window function nor an aggregate function",
 							NameListToString(funcname)),
 					 parser_errposition(pstate, location)));
+
+		if (fdresult == FUNCDETAIL_NORMAL && proc_call)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_FUNCTION),
+					 errmsg("%s is not a procedure",
+							func_signature_string(funcname, nargs,
+												  argnames,
+												  actual_arg_types)),
+					 errhint("To call a function, use SELECT."),
+					 parser_errposition(pstate, location)));
+
+		if (fdresult == FUNCDETAIL_PROCEDURE && !proc_call)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_FUNCTION),
+					 errmsg("%s is a procedure",
+							func_signature_string(funcname, nargs,
+												  argnames,
+												  actual_arg_types)),
+					 errhint("To call a procedure, use CALL."),
+					 parser_errposition(pstate, location)));
 	}
 	else if (fdresult == FUNCDETAIL_AGGREGATE)
 	{
@@ -635,7 +655,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 		check_srf_call_placement(pstate, last_srf, location);
 
 	/* build the appropriate output structure */
-	if (fdresult == FUNCDETAIL_NORMAL)
+	if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
 	{
 		FuncExpr   *funcexpr = makeNode(FuncExpr);
 
@@ -1589,6 +1609,8 @@ func_get_detail(List *funcname,
 			result = FUNCDETAIL_AGGREGATE;
 		else if (pform->proiswindow)
 			result = FUNCDETAIL_WINDOWFUNC;
+		else if (pform->prorettype == InvalidOid)
+			result = FUNCDETAIL_PROCEDURE;
 		else
 			result = FUNCDETAIL_NORMAL;
 		ReleaseSysCache(ftup);
@@ -1984,16 +2006,28 @@ LookupFuncName(List *funcname, int nargs, const Oid *argtypes, bool noError)
 
 /*
  * LookupFuncWithArgs
- *		Like LookupFuncName, but the argument types are specified by a
- *		ObjectWithArgs node.
+ *
+ * Like LookupFuncName, but the argument types are specified by a
+ * ObjectWithArgs node.  Also, this function can check whether the result is a
+ * function, procedure, or aggregate, based on the objtype argument.  Pass
+ * OBJECT_ROUTINE to accept any of them.
+ *
+ * For historical reasons, we also accept aggregates when looking for a
+ * function.
  */
 Oid
-LookupFuncWithArgs(ObjectWithArgs *func, bool noError)
+LookupFuncWithArgs(ObjectType objtype, ObjectWithArgs *func, bool noError)
 {
 	Oid			argoids[FUNC_MAX_ARGS];
 	int			argcount;
 	int			i;
 	ListCell   *args_item;
+	Oid			oid;
+
+	Assert(objtype == OBJECT_AGGREGATE ||
+		   objtype == OBJECT_FUNCTION ||
+		   objtype == OBJECT_PROCEDURE ||
+		   objtype == OBJECT_ROUTINE);
 
 	argcount = list_length(func->objargs);
 	if (argcount > FUNC_MAX_ARGS)
@@ -2013,90 +2047,100 @@ LookupFuncWithArgs(ObjectWithArgs *func, bool noError)
 		args_item = lnext(args_item);
 	}
 
-	return LookupFuncName(func->objname, func->args_unspecified ? -1 : argcount, argoids, noError);
-}
-
-/*
- * LookupAggWithArgs
- *		Find an aggregate function from a given ObjectWithArgs node.
- *
- * This is almost like LookupFuncWithArgs, but the error messages refer
- * to aggregates rather than plain functions, and we verify that the found
- * function really is an aggregate.
- */
-Oid
-LookupAggWithArgs(ObjectWithArgs *agg, bool noError)
-{
-	Oid			argoids[FUNC_MAX_ARGS];
-	int			argcount;
-	int			i;
-	ListCell   *lc;
-	Oid			oid;
-	HeapTuple	ftup;
-	Form_pg_proc pform;
-
-	argcount = list_length(agg->objargs);
-	if (argcount > FUNC_MAX_ARGS)
-		ereport(ERROR,
-				(errcode(ERRCODE_TOO_MANY_ARGUMENTS),
-				 errmsg_plural("functions cannot have more than %d argument",
-							   "functions cannot have more than %d arguments",
-							   FUNC_MAX_ARGS,
-							   FUNC_MAX_ARGS)));
+	/*
+	 * When looking for a function or routine, we pass noError through to
+	 * LookupFuncName and let it make any error messages.  Otherwise, we make
+	 * our own errors for the aggregate and procedure cases.
+	 */
+	oid = LookupFuncName(func->objname, func->args_unspecified ? -1 : argcount, argoids,
+						 (objtype == OBJECT_FUNCTION || objtype == OBJECT_ROUTINE) ? noError : true);
 
-	i = 0;
-	foreach(lc, agg->objargs)
+	if (objtype == OBJECT_FUNCTION)
 	{
-		TypeName   *t = (TypeName *) lfirst(lc);
-
-		argoids[i] = LookupTypeNameOid(NULL, t, noError);
-		i++;
+		/* Make sure it's a function, not a procedure */
+		if (oid && get_func_rettype(oid) == InvalidOid)
+		{
+			if (noError)
+				return InvalidOid;
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("%s is not a function",
+							func_signature_string(func->objname, argcount,
+												  NIL, argoids))));
+		}
 	}
-
-	oid = LookupFuncName(agg->objname, argcount, argoids, true);
-
-	if (!OidIsValid(oid))
+	else if (objtype == OBJECT_PROCEDURE)
 	{
-		if (noError)
-			return InvalidOid;
-		if (argcount == 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_UNDEFINED_FUNCTION),
-					 errmsg("aggregate %s(*) does not exist",
-							NameListToString(agg->objname))));
-		else
+		if (!OidIsValid(oid))
+		{
+			if (noError)
+				return InvalidOid;
+			else if (func->args_unspecified)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("could not find a procedure named \"%s\"",
+								NameListToString(func->objname))));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("procedure %s does not exist",
+								func_signature_string(func->objname, argcount,
+													  NIL, argoids))));
+		}
+
+		/* Make sure it's a procedure */
+		if (get_func_rettype(oid) != InvalidOid)
+		{
+			if (noError)
+				return InvalidOid;
 			ereport(ERROR,
-					(errcode(ERRCODE_UNDEFINED_FUNCTION),
-					 errmsg("aggregate %s does not exist",
-							func_signature_string(agg->objname, argcount,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("%s is not a procedure",
+							func_signature_string(func->objname, argcount,
 												  NIL, argoids))));
+		}
 	}
-
-	/* Make sure it's an aggregate */
-	ftup = SearchSysCache1(PROCOID, ObjectIdGetDatum(oid));
-	if (!HeapTupleIsValid(ftup))	/* should not happen */
-		elog(ERROR, "cache lookup failed for function %u", oid);
-	pform = (Form_pg_proc) GETSTRUCT(ftup);
-
-	if (!pform->proisagg)
+	else if (objtype == OBJECT_AGGREGATE)
 	{
-		ReleaseSysCache(ftup);
-		if (noError)
-			return InvalidOid;
-		/* we do not use the (*) notation for functions... */
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("function %s is not an aggregate",
-						func_signature_string(agg->objname, argcount,
-											  NIL, argoids))));
-	}
+		if (!OidIsValid(oid))
+		{
+			if (noError)
+				return InvalidOid;
+			else if (func->args_unspecified)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("could not find a aggregate named \"%s\"",
+								NameListToString(func->objname))));
+			else if (argcount == 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("aggregate %s(*) does not exist",
+								NameListToString(func->objname))));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("aggregate %s does not exist",
+								func_signature_string(func->objname, argcount,
+													  NIL, argoids))));
+		}
 
-	ReleaseSysCache(ftup);
+		/* Make sure it's an aggregate */
+		if (!get_func_isagg(oid))
+		{
+			if (noError)
+				return InvalidOid;
+			/* we do not use the (*) notation for functions... */
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("function %s is not an aggregate",
+							func_signature_string(func->objname, argcount,
+												  NIL, argoids))));
+		}
+	}
 
 	return oid;
 }
 
-
 /*
  * check_srf_call_placement
  *		Verify that a set-returning function is called in a valid place,
@@ -2236,6 +2280,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_PARTITION_EXPRESSION:
 			err = _("set-returning functions are not allowed in partition key expressions");
 			break;
+		case EXPR_KIND_CALL:
+			err = _("set-returning functions are not allowed in CALL arguments");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 82a707af7b..4da1f8f643 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -657,6 +657,10 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			}
 			break;
 
+		case T_CallStmt:
+			ExecuteCallStmt(pstate, castNode(CallStmt, parsetree));
+			break;
+
 		case T_ClusterStmt:
 			/* we choose to allow this during "read only" transactions */
 			PreventCommandDuringRecovery("CLUSTER");
@@ -1957,9 +1961,15 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_POLICY:
 			tag = "ALTER POLICY";
 			break;
+		case OBJECT_PROCEDURE:
+			tag = "ALTER PROCEDURE";
+			break;
 		case OBJECT_ROLE:
 			tag = "ALTER ROLE";
 			break;
+		case OBJECT_ROUTINE:
+			tag = "ALTER ROUTINE";
+			break;
 		case OBJECT_RULE:
 			tag = "ALTER RULE";
 			break;
@@ -2261,6 +2271,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_FUNCTION:
 					tag = "DROP FUNCTION";
 					break;
+				case OBJECT_PROCEDURE:
+					tag = "DROP PROCEDURE";
+					break;
+				case OBJECT_ROUTINE:
+					tag = "DROP ROUTINE";
+					break;
 				case OBJECT_AGGREGATE:
 					tag = "DROP AGGREGATE";
 					break;
@@ -2359,7 +2375,20 @@ CreateCommandTag(Node *parsetree)
 			break;
 
 		case T_AlterFunctionStmt:
-			tag = "ALTER FUNCTION";
+			switch (((AlterFunctionStmt *) parsetree)->objtype)
+			{
+				case OBJECT_FUNCTION:
+					tag = "ALTER FUNCTION";
+					break;
+				case OBJECT_PROCEDURE:
+					tag = "ALTER PROCEDURE";
+					break;
+				case OBJECT_ROUTINE:
+					tag = "ALTER ROUTINE";
+					break;
+				default:
+					tag = "???";
+			}
 			break;
 
 		case T_GrantStmt:
@@ -2438,7 +2467,10 @@ CreateCommandTag(Node *parsetree)
 			break;
 
 		case T_CreateFunctionStmt:
-			tag = "CREATE FUNCTION";
+			if (((CreateFunctionStmt *) parsetree)->is_procedure)
+				tag = "CREATE PROCEDURE";
+			else
+				tag = "CREATE FUNCTION";
 			break;
 
 		case T_IndexStmt:
@@ -2493,6 +2525,10 @@ CreateCommandTag(Node *parsetree)
 			tag = "LOAD";
 			break;
 
+		case T_CallStmt:
+			tag = "CALL";
+			break;
+
 		case T_ClusterStmt:
 			tag = "CLUSTER";
 			break;
@@ -3116,6 +3152,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_ALL;
 			break;
 
+		case T_CallStmt:
+			lev = LOGSTMT_ALL;
+			break;
+
 		case T_ClusterStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 06cf32f5d7..8514c21c40 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2691,6 +2691,12 @@ pg_get_function_result(PG_FUNCTION_ARGS)
 	if (!HeapTupleIsValid(proctup))
 		PG_RETURN_NULL();
 
+	if (((Form_pg_proc) GETSTRUCT(proctup))->prorettype == InvalidOid)
+	{
+		ReleaseSysCache(proctup);
+		PG_RETURN_NULL();
+	}
+
 	initStringInfo(&buf);
 
 	print_function_rettype(&buf, proctup);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 0ea2f2bc54..5211360777 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1614,6 +1614,25 @@ func_parallel(Oid funcid)
 	return result;
 }
 
+/*
+ * get_func_isagg
+ *	   Given procedure id, return the function's proisagg field.
+ */
+bool
+get_func_isagg(Oid funcid)
+{
+	HeapTuple	tp;
+	bool		result;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+
+	result = ((Form_pg_proc) GETSTRUCT(tp))->proisagg;
+	ReleaseSysCache(tp);
+	return result;
+}
+
 /*
  * get_func_leakproof
  *	   Given procedure id, return the function's leakproof field.
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 70d8f24d17..12290a1aae 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -33,7 +33,7 @@ static void AddAcl(PQExpBuffer aclbuf, const char *keyword,
  *	name: the object name, in the form to use in the commands (already quoted)
  *	subname: the sub-object name, if any (already quoted); NULL if none
  *	type: the object type (as seen in GRANT command: must be one of
- *		TABLE, SEQUENCE, FUNCTION, LANGUAGE, SCHEMA, DATABASE, TABLESPACE,
+ *		TABLE, SEQUENCE, FUNCTION, PROCEDURE, LANGUAGE, SCHEMA, DATABASE, TABLESPACE,
  *		FOREIGN DATA WRAPPER, SERVER, or LARGE OBJECT)
  *	acls: the ACL string fetched from the database
  *	racls: the ACL string of any initial-but-now-revoked privileges
@@ -524,6 +524,9 @@ do { \
 	else if (strcmp(type, "FUNCTION") == 0 ||
 			 strcmp(type, "FUNCTIONS") == 0)
 		CONVERT_PRIV('X', "EXECUTE");
+	else if (strcmp(type, "PROCEDURE") == 0 ||
+			 strcmp(type, "PROCEDURES") == 0)
+		CONVERT_PRIV('X', "EXECUTE");
 	else if (strcmp(type, "LANGUAGE") == 0)
 		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "SCHEMA") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index ec2fa8b9b9..41741aefbc 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2889,7 +2889,8 @@ _tocEntryRequired(TocEntry *te, teSection curSection, RestoreOptions *ropt)
 			if (ropt->indexNames.head != NULL && (!(simple_string_list_member(&ropt->indexNames, te->tag))))
 				return 0;
 		}
-		else if (strcmp(te->desc, "FUNCTION") == 0)
+		else if (strcmp(te->desc, "FUNCTION") == 0 ||
+				 strcmp(te->desc, "PROCEDURE") == 0)
 		{
 			if (!ropt->selFunction)
 				return 0;
@@ -3388,7 +3389,8 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te, ArchiveHandle *AH)
 		strcmp(type, "FUNCTION") == 0 ||
 		strcmp(type, "OPERATOR") == 0 ||
 		strcmp(type, "OPERATOR CLASS") == 0 ||
-		strcmp(type, "OPERATOR FAMILY") == 0)
+		strcmp(type, "OPERATOR FAMILY") == 0 ||
+		strcmp(type, "PROCEDURE") == 0)
 	{
 		/* Chop "DROP " off the front and make a modifiable copy */
 		char	   *first = pg_strdup(te->dropStmt + 5);
@@ -3560,6 +3562,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 			strcmp(te->desc, "OPERATOR") == 0 ||
 			strcmp(te->desc, "OPERATOR CLASS") == 0 ||
 			strcmp(te->desc, "OPERATOR FAMILY") == 0 ||
+			strcmp(te->desc, "PROCEDURE") == 0 ||
 			strcmp(te->desc, "PROCEDURAL LANGUAGE") == 0 ||
 			strcmp(te->desc, "SCHEMA") == 0 ||
 			strcmp(te->desc, "EVENT TRIGGER") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d8fb356130..e6701aaa78 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11349,6 +11349,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 	char	   *funcargs;
 	char	   *funciargs;
 	char	   *funcresult;
+	bool		is_procedure;
 	char	   *proallargtypes;
 	char	   *proargmodes;
 	char	   *proargnames;
@@ -11370,6 +11371,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 	char	  **argnames = NULL;
 	char	  **configitems = NULL;
 	int			nconfigitems = 0;
+	const char *keyword;
 	int			i;
 
 	/* Skip if not to be dumped */
@@ -11513,7 +11515,11 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 	{
 		funcargs = PQgetvalue(res, 0, PQfnumber(res, "funcargs"));
 		funciargs = PQgetvalue(res, 0, PQfnumber(res, "funciargs"));
-		funcresult = PQgetvalue(res, 0, PQfnumber(res, "funcresult"));
+		is_procedure = PQgetisnull(res, 0, PQfnumber(res, "funcresult"));
+		if (is_procedure)
+			funcresult = NULL;
+		else
+			funcresult = PQgetvalue(res, 0, PQfnumber(res, "funcresult"));
 		proallargtypes = proargmodes = proargnames = NULL;
 	}
 	else
@@ -11522,6 +11528,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 		proargmodes = PQgetvalue(res, 0, PQfnumber(res, "proargmodes"));
 		proargnames = PQgetvalue(res, 0, PQfnumber(res, "proargnames"));
 		funcargs = funciargs = funcresult = NULL;
+		is_procedure = false;
 	}
 	if (PQfnumber(res, "protrftypes") != -1)
 		protrftypes = PQgetvalue(res, 0, PQfnumber(res, "protrftypes"));
@@ -11653,22 +11660,29 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 
 	funcsig_tag = format_function_signature(fout, finfo, false);
 
+	keyword = is_procedure ? "PROCEDURE" : "FUNCTION";
+
 	/*
 	 * DROP must be fully qualified in case same name appears in pg_catalog
 	 */
-	appendPQExpBuffer(delqry, "DROP FUNCTION %s.%s;\n",
+	appendPQExpBuffer(delqry, "DROP %s %s.%s;\n",
+					  keyword,
 					  fmtId(finfo->dobj.namespace->dobj.name),
 					  funcsig);
 
-	appendPQExpBuffer(q, "CREATE FUNCTION %s ", funcfullsig ? funcfullsig :
+	appendPQExpBuffer(q, "CREATE %s %s",
+					  keyword,
+					  funcfullsig ? funcfullsig :
 					  funcsig);
-	if (funcresult)
-		appendPQExpBuffer(q, "RETURNS %s", funcresult);
+	if (is_procedure)
+		;
+	else if (funcresult)
+		appendPQExpBuffer(q, " RETURNS %s", funcresult);
 	else
 	{
 		rettypename = getFormattedTypeName(fout, finfo->prorettype,
 										   zeroAsOpaque);
-		appendPQExpBuffer(q, "RETURNS %s%s",
+		appendPQExpBuffer(q, " RETURNS %s%s",
 						  (proretset[0] == 't') ? "SETOF " : "",
 						  rettypename);
 		free(rettypename);
@@ -11775,7 +11789,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 
 	appendPQExpBuffer(q, "\n    %s;\n", asPart->data);
 
-	appendPQExpBuffer(labelq, "FUNCTION %s", funcsig);
+	appendPQExpBuffer(labelq, "%s %s", keyword, funcsig);
 
 	if (dopt->binary_upgrade)
 		binary_upgrade_extension_member(q, &finfo->dobj, labelq->data);
@@ -11786,7 +11800,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 					 finfo->dobj.namespace->dobj.name,
 					 NULL,
 					 finfo->rolname, false,
-					 "FUNCTION", SECTION_PRE_DATA,
+					 keyword, SECTION_PRE_DATA,
 					 q->data, delqry->data, NULL,
 					 NULL, 0,
 					 NULL, NULL);
@@ -11803,7 +11817,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 					 finfo->dobj.catId, 0, finfo->dobj.dumpId);
 
 	if (finfo->dobj.dump & DUMP_COMPONENT_ACL)
-		dumpACL(fout, finfo->dobj.catId, finfo->dobj.dumpId, "FUNCTION",
+		dumpACL(fout, finfo->dobj.catId, finfo->dobj.dumpId, keyword,
 				funcsig, NULL, funcsig_tag,
 				finfo->dobj.namespace->dobj.name,
 				finfo->rolname, finfo->proacl, finfo->rproacl,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index fa3b56a426..7cf9bdadb2 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3654,6 +3654,44 @@
 			section_data             => 1,
 			section_post_data        => 1, }, },
 
+	'CREATE PROCEDURE dump_test.ptest1' => {
+		all_runs     => 1,
+		create_order => 41,
+		create_sql   => 'CREATE PROCEDURE dump_test.ptest1(a int)
+					   LANGUAGE SQL AS $$ INSERT INTO dump_test.test_table (col1) VALUES (a) $$;',
+		regexp => qr/^
+			\QCREATE PROCEDURE ptest1(a integer)\E
+			\n\s+\QLANGUAGE sql\E
+			\n\s+AS\ \$\$\Q INSERT INTO dump_test.test_table (col1) VALUES (a) \E\$\$;
+			/xm,
+		like => {
+			binary_upgrade          => 1,
+			clean                   => 1,
+			clean_if_exists         => 1,
+			createdb                => 1,
+			defaults                => 1,
+			exclude_test_table      => 1,
+			exclude_test_table_data => 1,
+			no_blobs                => 1,
+			no_privs                => 1,
+			no_owner                => 1,
+			only_dump_test_schema   => 1,
+			pg_dumpall_dbprivs      => 1,
+			schema_only             => 1,
+			section_pre_data        => 1,
+			test_schema_plus_blobs  => 1,
+			with_oids               => 1, },
+		unlike => {
+			column_inserts           => 1,
+			data_only                => 1,
+			exclude_dump_test_schema => 1,
+			only_dump_test_table     => 1,
+			pg_dumpall_globals       => 1,
+			pg_dumpall_globals_clean => 1,
+			role                     => 1,
+			section_data             => 1,
+			section_post_data        => 1, }, },
+
 	'CREATE TYPE dump_test.int42 populated' => {
 		all_runs     => 1,
 		create_order => 42,
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index b7b978a361..0b0644a653 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -353,6 +353,7 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 						  " CASE\n"
 						  "  WHEN p.proisagg THEN '%s'\n"
 						  "  WHEN p.proiswindow THEN '%s'\n"
+						  "  WHEN p.prorettype = 0 THEN '%s'\n"
 						  "  WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN '%s'\n"
 						  "  ELSE '%s'\n"
 						  " END as \"%s\"",
@@ -361,8 +362,9 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* translator: "agg" is short for "aggregate" */
 						  gettext_noop("agg"),
 						  gettext_noop("window"),
+						  gettext_noop("proc"),
 						  gettext_noop("trigger"),
-						  gettext_noop("normal"),
+						  gettext_noop("func"),
 						  gettext_noop("Type"));
 	else if (pset.sversion >= 80100)
 		appendPQExpBuffer(&buf,
@@ -407,7 +409,7 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* translator: "agg" is short for "aggregate" */
 						  gettext_noop("agg"),
 						  gettext_noop("trigger"),
-						  gettext_noop("normal"),
+						  gettext_noop("func"),
 						  gettext_noop("Type"));
 	else
 		appendPQExpBuffer(&buf,
@@ -424,7 +426,7 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* translator: "agg" is short for "aggregate" */
 						  gettext_noop("agg"),
 						  gettext_noop("trigger"),
-						  gettext_noop("normal"),
+						  gettext_noop("func"),
 						  gettext_noop("Type"));
 
 	if (verbose)
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b3e3799c13..468e50aa31 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -397,7 +397,7 @@ static const SchemaQuery Query_for_list_of_functions = {
 	/* catname */
 	"pg_catalog.pg_proc p",
 	/* selcondition */
-	NULL,
+	"p.prorettype <> 0",
 	/* viscondition */
 	"pg_catalog.pg_function_is_visible(p.oid)",
 	/* namespace */
@@ -423,6 +423,36 @@ static const SchemaQuery Query_for_list_of_indexes = {
 	NULL
 };
 
+static const SchemaQuery Query_for_list_of_procedures = {
+	/* catname */
+	"pg_catalog.pg_proc p",
+	/* selcondition */
+	"p.prorettype = 0",
+	/* viscondition */
+	"pg_catalog.pg_function_is_visible(p.oid)",
+	/* namespace */
+	"p.pronamespace",
+	/* result */
+	"pg_catalog.quote_ident(p.proname)",
+	/* qualresult */
+	NULL
+};
+
+static const SchemaQuery Query_for_list_of_routines = {
+	/* catname */
+	"pg_catalog.pg_proc p",
+	/* selcondition */
+	NULL,
+	/* viscondition */
+	"pg_catalog.pg_function_is_visible(p.oid)",
+	/* namespace */
+	"p.pronamespace",
+	/* result */
+	"pg_catalog.quote_ident(p.proname)",
+	/* qualresult */
+	NULL
+};
+
 static const SchemaQuery Query_for_list_of_sequences = {
 	/* catname */
 	"pg_catalog.pg_class c",
@@ -1032,8 +1062,10 @@ static const pgsql_thing_t words_after_create[] = {
 	{"OWNED", NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},	/* for DROP OWNED BY ... */
 	{"PARSER", Query_for_list_of_ts_parsers, NULL, THING_NO_SHOW},
 	{"POLICY", NULL, NULL},
+	{"PROCEDURE", NULL, &Query_for_list_of_procedures},
 	{"PUBLICATION", Query_for_list_of_publications},
 	{"ROLE", Query_for_list_of_roles},
+	{"ROUTINE", NULL, &Query_for_list_of_routines, THING_NO_CREATE},
 	{"RULE", "SELECT pg_catalog.quote_ident(rulename) FROM pg_catalog.pg_rules WHERE substring(pg_catalog.quote_ident(rulename),1,%d)='%s'"},
 	{"SCHEMA", Query_for_list_of_schemas},
 	{"SEQUENCE", NULL, &Query_for_list_of_sequences},
@@ -1407,7 +1439,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* Known command-starting keywords. */
 	static const char *const sql_commands[] = {
-		"ABORT", "ALTER", "ANALYZE", "BEGIN", "CHECKPOINT", "CLOSE", "CLUSTER",
+		"ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER",
 		"COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE",
 		"DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN",
 		"FETCH", "GRANT", "IMPORT", "INSERT", "LISTEN", "LOAD", "LOCK",
@@ -1520,11 +1552,11 @@ psql_completion(const char *text, int start, int end)
 	/* ALTER TABLE,INDEX,MATERIALIZED VIEW ALL IN TABLESPACE xxx OWNED BY xxx */
 	else if (TailMatches7("ALL", "IN", "TABLESPACE", MatchAny, "OWNED", "BY", MatchAny))
 		COMPLETE_WITH_CONST("SET TABLESPACE");
-	/* ALTER AGGREGATE,FUNCTION <name> */
-	else if (Matches3("ALTER", "AGGREGATE|FUNCTION", MatchAny))
+	/* ALTER AGGREGATE,FUNCTION,PROCEDURE,ROUTINE <name> */
+	else if (Matches3("ALTER", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny))
 		COMPLETE_WITH_CONST("(");
-	/* ALTER AGGREGATE,FUNCTION <name> (...) */
-	else if (Matches4("ALTER", "AGGREGATE|FUNCTION", MatchAny, MatchAny))
+	/* ALTER AGGREGATE,FUNCTION,PROCEDURE,ROUTINE <name> (...) */
+	else if (Matches4("ALTER", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny))
 	{
 		if (ends_with(prev_wd, ')'))
 			COMPLETE_WITH_LIST3("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2145,6 +2177,11 @@ psql_completion(const char *text, int start, int end)
 /* ROLLBACK */
 	else if (Matches1("ROLLBACK"))
 		COMPLETE_WITH_LIST4("WORK", "TRANSACTION", "TO SAVEPOINT", "PREPARED");
+/* CALL */
+	else if (Matches1("CALL"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+	else if (Matches2("CALL", MatchAny))
+		COMPLETE_WITH_CONST("(");
 /* CLUSTER */
 	else if (Matches1("CLUSTER"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, "UNION SELECT 'VERBOSE'");
@@ -2176,6 +2213,7 @@ psql_completion(const char *text, int start, int end)
 			"SERVER", "INDEX", "LANGUAGE", "POLICY", "PUBLICATION", "RULE",
 			"SCHEMA", "SEQUENCE", "STATISTICS", "SUBSCRIPTION",
 			"TABLE", "TYPE", "VIEW", "MATERIALIZED VIEW", "COLUMN", "AGGREGATE", "FUNCTION",
+			"PROCEDURE", "ROUTINE",
 			"OPERATOR", "TRIGGER", "CONSTRAINT", "DOMAIN", "LARGE OBJECT",
 		"TABLESPACE", "TEXT SEARCH", "ROLE", NULL};
 
@@ -2685,7 +2723,7 @@ psql_completion(const char *text, int start, int end)
 					  "COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW",
 					  MatchAny) ||
 			 Matches4("DROP", "ACCESS", "METHOD", MatchAny) ||
-			 (Matches4("DROP", "AGGREGATE|FUNCTION", MatchAny, MatchAny) &&
+			 (Matches4("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
 			 Matches4("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches5("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
@@ -2694,9 +2732,9 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH_LIST2("CASCADE", "RESTRICT");
 
 	/* help completing some of the variants */
-	else if (Matches3("DROP", "AGGREGATE|FUNCTION", MatchAny))
+	else if (Matches3("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny))
 		COMPLETE_WITH_CONST("(");
-	else if (Matches4("DROP", "AGGREGATE|FUNCTION", MatchAny, "("))
+	else if (Matches4("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, "("))
 		COMPLETE_WITH_FUNCTION_ARG(prev2_wd);
 	else if (Matches2("DROP", "FOREIGN"))
 		COMPLETE_WITH_LIST2("DATA WRAPPER", "TABLE");
@@ -2893,10 +2931,12 @@ psql_completion(const char *text, int start, int end)
 		 * objects supported.
 		 */
 		if (HeadMatches3("ALTER", "DEFAULT", "PRIVILEGES"))
-			COMPLETE_WITH_LIST5("TABLES", "SEQUENCES", "FUNCTIONS", "TYPES", "SCHEMAS");
+			COMPLETE_WITH_LIST7("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS");
 		else
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvmf,
 									   " UNION SELECT 'ALL FUNCTIONS IN SCHEMA'"
+									   " UNION SELECT 'ALL PROCEDURES IN SCHEMA'"
+									   " UNION SELECT 'ALL ROUTINES IN SCHEMA'"
 									   " UNION SELECT 'ALL SEQUENCES IN SCHEMA'"
 									   " UNION SELECT 'ALL TABLES IN SCHEMA'"
 									   " UNION SELECT 'DATABASE'"
@@ -2906,6 +2946,8 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'FUNCTION'"
 									   " UNION SELECT 'LANGUAGE'"
 									   " UNION SELECT 'LARGE OBJECT'"
+									   " UNION SELECT 'PROCEDURE'"
+									   " UNION SELECT 'ROUTINE'"
 									   " UNION SELECT 'SCHEMA'"
 									   " UNION SELECT 'SEQUENCE'"
 									   " UNION SELECT 'TABLE'"
@@ -2913,7 +2955,10 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'TYPE'");
 	}
 	else if (TailMatches4("GRANT|REVOKE", MatchAny, "ON", "ALL"))
-		COMPLETE_WITH_LIST3("FUNCTIONS IN SCHEMA", "SEQUENCES IN SCHEMA",
+		COMPLETE_WITH_LIST5("FUNCTIONS IN SCHEMA",
+							"PROCEDURES IN SCHEMA",
+							"ROUTINES IN SCHEMA",
+							"SEQUENCES IN SCHEMA",
 							"TABLES IN SCHEMA");
 	else if (TailMatches4("GRANT|REVOKE", MatchAny, "ON", "FOREIGN"))
 		COMPLETE_WITH_LIST2("DATA WRAPPER", "SERVER");
@@ -2934,6 +2979,10 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
 		else if (TailMatches1("LANGUAGE"))
 			COMPLETE_WITH_QUERY(Query_for_list_of_languages);
+		else if (TailMatches1("PROCEDURE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+		else if (TailMatches1("ROUTINE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
 		else if (TailMatches1("SCHEMA"))
 			COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
 		else if (TailMatches1("SEQUENCE"))
@@ -3163,7 +3212,7 @@ psql_completion(const char *text, int start, int end)
 		static const char *const list_SECURITY_LABEL[] =
 		{"TABLE", "COLUMN", "AGGREGATE", "DATABASE", "DOMAIN",
 			"EVENT TRIGGER", "FOREIGN TABLE", "FUNCTION", "LARGE OBJECT",
-			"MATERIALIZED VIEW", "LANGUAGE", "PUBLICATION", "ROLE", "SCHEMA",
+			"MATERIALIZED VIEW", "LANGUAGE", "PUBLICATION", "PROCEDURE", "ROLE", "ROUTINE", "SCHEMA",
 		"SEQUENCE", "SUBSCRIPTION", "TABLESPACE", "TYPE", "VIEW", NULL};
 
 		COMPLETE_WITH_LIST(list_SECURITY_LABEL);
@@ -3233,8 +3282,8 @@ psql_completion(const char *text, int start, int end)
 	/* Complete SET <var> with "TO" */
 	else if (Matches2("SET", MatchAny))
 		COMPLETE_WITH_CONST("TO");
-	/* Complete ALTER DATABASE|FUNCTION|ROLE|USER ... SET <name> */
-	else if (HeadMatches2("ALTER", "DATABASE|FUNCTION|ROLE|USER") &&
+	/* Complete ALTER DATABASE|FUNCTION||PROCEDURE|ROLE|ROUTINE|USER ... SET <name> */
+	else if (HeadMatches2("ALTER", "DATABASE|FUNCTION|PROCEDURE|ROLE|ROUTINE|USER") &&
 			 TailMatches2("SET", MatchAny))
 		COMPLETE_WITH_LIST2("FROM CURRENT", "TO");
 	/* Suggest possible variable values */
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index bfead9af3d..52cbf61ccb 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -59,12 +59,13 @@ extern void DropTransformById(Oid transformOid);
 extern void IsThereFunctionInNamespace(const char *proname, int pronargs,
 						   oidvector *proargtypes, Oid nspOid);
 extern void ExecuteDoStmt(DoStmt *stmt);
+extern void ExecuteCallStmt(ParseState *pstate, CallStmt *stmt);
 extern Oid	get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok);
 extern Oid	get_transform_oid(Oid type_id, Oid lang_id, bool missing_ok);
 extern void interpret_function_parameter_list(ParseState *pstate,
 								  List *parameters,
 								  Oid languageOid,
-								  bool is_aggregate,
+								  ObjectType objtype,
 								  oidvector **parameterTypes,
 								  ArrayType **allParameterTypes,
 								  ArrayType **parameterModes,
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index ffeeb4919b..43ee88bd39 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -413,6 +413,7 @@ typedef enum NodeTag
 	T_DropSubscriptionStmt,
 	T_CreateStatsStmt,
 	T_AlterCollationStmt,
+	T_CallStmt,
 
 	/*
 	 * TAGS FOR PARSE TREE NODES (parsenodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 34d6afc80f..c4ff1c5544 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -355,6 +355,7 @@ typedef struct FuncCall
 	bool		agg_distinct;	/* arguments were labeled DISTINCT */
 	bool		func_variadic;	/* last argument was labeled VARIADIC */
 	struct WindowDef *over;		/* OVER clause, if any */
+	bool		proc_call;		/* CALL statement */
 	int			location;		/* token location, or -1 if unknown */
 } FuncCall;
 
@@ -1642,9 +1643,11 @@ typedef enum ObjectType
 	OBJECT_OPERATOR,
 	OBJECT_OPFAMILY,
 	OBJECT_POLICY,
+	OBJECT_PROCEDURE,
 	OBJECT_PUBLICATION,
 	OBJECT_PUBLICATION_REL,
 	OBJECT_ROLE,
+	OBJECT_ROUTINE,
 	OBJECT_RULE,
 	OBJECT_SCHEMA,
 	OBJECT_SEQUENCE,
@@ -1856,6 +1859,8 @@ typedef enum GrantObjectType
 	ACL_OBJECT_LANGUAGE,		/* procedural language */
 	ACL_OBJECT_LARGEOBJECT,		/* largeobject */
 	ACL_OBJECT_NAMESPACE,		/* namespace */
+	ACL_OBJECT_PROCEDURE,		/* procedure */
+	ACL_OBJECT_ROUTINE,			/* routine */
 	ACL_OBJECT_TABLESPACE,		/* tablespace */
 	ACL_OBJECT_TYPE				/* type */
 } GrantObjectType;
@@ -2749,6 +2754,7 @@ typedef struct CreateFunctionStmt
 	List	   *funcname;		/* qualified name of function to create */
 	List	   *parameters;		/* a list of FunctionParameter */
 	TypeName   *returnType;		/* the return type */
+	bool		is_procedure;
 	List	   *options;		/* a list of DefElem */
 	List	   *withClause;		/* a list of DefElem */
 } CreateFunctionStmt;
@@ -2775,6 +2781,7 @@ typedef struct FunctionParameter
 typedef struct AlterFunctionStmt
 {
 	NodeTag		type;
+	ObjectType	objtype;
 	ObjectWithArgs *func;		/* name and args of function */
 	List	   *actions;		/* list of DefElem */
 } AlterFunctionStmt;
@@ -2799,6 +2806,16 @@ typedef struct InlineCodeBlock
 	bool		langIsTrusted;	/* trusted property of the language */
 } InlineCodeBlock;
 
+/* ----------------------
+ *		CALL statement
+ * ----------------------
+ */
+typedef struct CallStmt
+{
+	NodeTag		type;
+	FuncCall   *funccall;
+} CallStmt;
+
 /* ----------------------
  *		Alter Object Rename Statement
  * ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f50e45e886..a932400058 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -63,6 +63,7 @@ PG_KEYWORD("boolean", BOOLEAN_P, COL_NAME_KEYWORD)
 PG_KEYWORD("both", BOTH, RESERVED_KEYWORD)
 PG_KEYWORD("by", BY, UNRESERVED_KEYWORD)
 PG_KEYWORD("cache", CACHE, UNRESERVED_KEYWORD)
+PG_KEYWORD("call", CALL, UNRESERVED_KEYWORD)
 PG_KEYWORD("called", CALLED, UNRESERVED_KEYWORD)
 PG_KEYWORD("cascade", CASCADE, UNRESERVED_KEYWORD)
 PG_KEYWORD("cascaded", CASCADED, UNRESERVED_KEYWORD)
@@ -310,6 +311,7 @@ PG_KEYWORD("prior", PRIOR, UNRESERVED_KEYWORD)
 PG_KEYWORD("privileges", PRIVILEGES, UNRESERVED_KEYWORD)
 PG_KEYWORD("procedural", PROCEDURAL, UNRESERVED_KEYWORD)
 PG_KEYWORD("procedure", PROCEDURE, UNRESERVED_KEYWORD)
+PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD)
 PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD)
 PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD)
 PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD)
@@ -340,6 +342,8 @@ PG_KEYWORD("right", RIGHT, TYPE_FUNC_NAME_KEYWORD)
 PG_KEYWORD("role", ROLE, UNRESERVED_KEYWORD)
 PG_KEYWORD("rollback", ROLLBACK, UNRESERVED_KEYWORD)
 PG_KEYWORD("rollup", ROLLUP, UNRESERVED_KEYWORD)
+PG_KEYWORD("routine", ROUTINE, UNRESERVED_KEYWORD)
+PG_KEYWORD("routines", ROUTINES, UNRESERVED_KEYWORD)
 PG_KEYWORD("row", ROW, COL_NAME_KEYWORD)
 PG_KEYWORD("rows", ROWS, UNRESERVED_KEYWORD)
 PG_KEYWORD("rule", RULE, UNRESERVED_KEYWORD)
diff --git a/src/include/parser/parse_func.h b/src/include/parser/parse_func.h
index b4b6084b1b..fccccd21ed 100644
--- a/src/include/parser/parse_func.h
+++ b/src/include/parser/parse_func.h
@@ -24,6 +24,7 @@ typedef enum
 	FUNCDETAIL_NOTFOUND,		/* no matching function */
 	FUNCDETAIL_MULTIPLE,		/* too many matching functions */
 	FUNCDETAIL_NORMAL,			/* found a matching regular function */
+	FUNCDETAIL_PROCEDURE,		/* found a matching procedure */
 	FUNCDETAIL_AGGREGATE,		/* found a matching aggregate function */
 	FUNCDETAIL_WINDOWFUNC,		/* found a matching window function */
 	FUNCDETAIL_COERCION			/* it's a type coercion request */
@@ -31,7 +32,8 @@ typedef enum
 
 
 extern Node *ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
-				  Node *last_srf, FuncCall *fn, int location);
+							   Node *last_srf, FuncCall *fn, bool proc_call,
+							   int location);
 
 extern FuncDetailCode func_get_detail(List *funcname,
 				List *fargs, List *fargnames,
@@ -62,10 +64,8 @@ extern const char *func_signature_string(List *funcname, int nargs,
 
 extern Oid LookupFuncName(List *funcname, int nargs, const Oid *argtypes,
 			   bool noError);
-extern Oid LookupFuncWithArgs(ObjectWithArgs *func,
+extern Oid LookupFuncWithArgs(ObjectType objtype, ObjectWithArgs *func,
 				   bool noError);
-extern Oid LookupAggWithArgs(ObjectWithArgs *agg,
-				  bool noError);
 
 extern void check_srf_call_placement(ParseState *pstate, Node *last_srf,
 						 int location);
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f0e210ad8d..565bb3dc6c 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -67,7 +67,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_EXECUTE_PARAMETER,	/* parameter value in EXECUTE */
 	EXPR_KIND_TRIGGER_WHEN,		/* WHEN condition in CREATE TRIGGER */
 	EXPR_KIND_POLICY,			/* USING or WITH CHECK expr in policy */
-	EXPR_KIND_PARTITION_EXPRESSION	/* PARTITION BY expression */
+	EXPR_KIND_PARTITION_EXPRESSION,	/* PARTITION BY expression */
+	EXPR_KIND_CALL				/* CALL argument */
 } ParseExprKind;
 
 
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 07208b56ce..b316cc594c 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -118,6 +118,7 @@ extern bool get_func_retset(Oid funcid);
 extern bool func_strict(Oid funcid);
 extern char func_volatile(Oid funcid);
 extern char func_parallel(Oid funcid);
+extern bool get_func_isagg(Oid funcid);
 extern bool get_func_leakproof(Oid funcid);
 extern float4 get_func_cost(Oid funcid);
 extern float4 get_func_rows(Oid funcid);
diff --git a/src/interfaces/ecpg/preproc/ecpg.tokens b/src/interfaces/ecpg/preproc/ecpg.tokens
index 68ba925efe..1d613af02f 100644
--- a/src/interfaces/ecpg/preproc/ecpg.tokens
+++ b/src/interfaces/ecpg/preproc/ecpg.tokens
@@ -2,7 +2,7 @@
 
 /* special embedded SQL tokens */
 %token  SQL_ALLOCATE SQL_AUTOCOMMIT SQL_BOOL SQL_BREAK
-                SQL_CALL SQL_CARDINALITY SQL_CONNECT
+                SQL_CARDINALITY SQL_CONNECT
                 SQL_COUNT
                 SQL_DATETIME_INTERVAL_CODE
                 SQL_DATETIME_INTERVAL_PRECISION SQL_DESCRIBE
diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer
index f60a62099d..19dc781885 100644
--- a/src/interfaces/ecpg/preproc/ecpg.trailer
+++ b/src/interfaces/ecpg/preproc/ecpg.trailer
@@ -1460,13 +1460,13 @@ action : CONTINUE_P
 			$<action>$.command = NULL;
 			$<action>$.str = mm_strdup("continue");
 		}
-		| SQL_CALL name '(' c_args ')'
+		| CALL name '(' c_args ')'
 		{
 			$<action>$.code = W_DO;
 			$<action>$.command = cat_str(4, $2, mm_strdup("("), $4, mm_strdup(")"));
 			$<action>$.str = cat2_str(mm_strdup("call"), mm_strdup($<action>$.command));
 		}
-		| SQL_CALL name
+		| CALL name
 		{
 			$<action>$.code = W_DO;
 			$<action>$.command = cat2_str($2, mm_strdup("()"));
@@ -1482,7 +1482,6 @@ ECPGKeywords: ECPGKeywords_vanames	{ $$ = $1; }
 		;
 
 ECPGKeywords_vanames:  SQL_BREAK		{ $$ = mm_strdup("break"); }
-		| SQL_CALL						{ $$ = mm_strdup("call"); }
 		| SQL_CARDINALITY				{ $$ = mm_strdup("cardinality"); }
 		| SQL_COUNT						{ $$ = mm_strdup("count"); }
 		| SQL_DATETIME_INTERVAL_CODE	{ $$ = mm_strdup("datetime_interval_code"); }
diff --git a/src/interfaces/ecpg/preproc/ecpg_keywords.c b/src/interfaces/ecpg/preproc/ecpg_keywords.c
index 3b52b8f3a2..848b2d4849 100644
--- a/src/interfaces/ecpg/preproc/ecpg_keywords.c
+++ b/src/interfaces/ecpg/preproc/ecpg_keywords.c
@@ -33,7 +33,6 @@ static const ScanKeyword ECPGScanKeywords[] = {
 	{"autocommit", SQL_AUTOCOMMIT, 0},
 	{"bool", SQL_BOOL, 0},
 	{"break", SQL_BREAK, 0},
-	{"call", SQL_CALL, 0},
 	{"cardinality", SQL_CARDINALITY, 0},
 	{"connect", SQL_CONNECT, 0},
 	{"count", SQL_COUNT, 0},
diff --git a/src/pl/plperl/GNUmakefile b/src/pl/plperl/GNUmakefile
index 91d1296b21..b829027d05 100644
--- a/src/pl/plperl/GNUmakefile
+++ b/src/pl/plperl/GNUmakefile
@@ -55,7 +55,7 @@ endif # win32
 SHLIB_LINK = $(perl_embed_ldflags)
 
 REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=plperl  --load-extension=plperlu
-REGRESS = plperl plperl_lc plperl_trigger plperl_shared plperl_elog plperl_util plperl_init plperlu plperl_array
+REGRESS = plperl plperl_lc plperl_trigger plperl_shared plperl_elog plperl_util plperl_init plperlu plperl_array plperl_call
 # if Perl can support two interpreters in one backend,
 # test plperl-and-plperlu cases
 ifneq ($(PERL),)
diff --git a/src/pl/plperl/expected/plperl_call.out b/src/pl/plperl/expected/plperl_call.out
new file mode 100644
index 0000000000..4bccfcb7c8
--- /dev/null
+++ b/src/pl/plperl/expected/plperl_call.out
@@ -0,0 +1,29 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE plperl
+AS $$
+undef;
+$$;
+CALL test_proc1();
+CREATE PROCEDURE test_proc2()
+LANGUAGE plperl
+AS $$
+return 5
+$$;
+CALL test_proc2();
+CREATE TABLE test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plperl
+AS $$
+spi_exec_query("INSERT INTO test1 VALUES ($_[0])");
+$$;
+CALL test_proc3(55);
+SELECT * FROM test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE test1;
diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index a57393fbdd..9f5313235f 100644
--- a/src/pl/plperl/plperl.c
+++ b/src/pl/plperl/plperl.c
@@ -1915,7 +1915,7 @@ plperl_inline_handler(PG_FUNCTION_ARGS)
 	desc.fn_retistuple = false;
 	desc.fn_retisset = false;
 	desc.fn_retisarray = false;
-	desc.result_oid = VOIDOID;
+	desc.result_oid = InvalidOid;
 	desc.nargs = 0;
 	desc.reference = NULL;
 
@@ -2481,7 +2481,7 @@ plperl_func_handler(PG_FUNCTION_ARGS)
 		}
 		retval = (Datum) 0;
 	}
-	else
+	else if (prodesc->result_oid)
 	{
 		retval = plperl_sv_to_datum(perlret,
 									prodesc->result_oid,
@@ -2826,7 +2826,7 @@ compile_plperl_function(Oid fn_oid, bool is_trigger, bool is_event_trigger)
 		 * Get the required information for input conversion of the
 		 * return value.
 		 ************************************************************/
-		if (!is_trigger && !is_event_trigger)
+		if (!is_trigger && !is_event_trigger && procStruct->prorettype)
 		{
 			Oid			rettype = procStruct->prorettype;
 
@@ -3343,7 +3343,7 @@ plperl_return_next_internal(SV *sv)
 
 		tuplestore_puttuple(current_call_data->tuple_store, tuple);
 	}
-	else
+	else if (prodesc->result_oid)
 	{
 		Datum		ret[1];
 		bool		isNull[1];
diff --git a/src/pl/plperl/sql/plperl_call.sql b/src/pl/plperl/sql/plperl_call.sql
new file mode 100644
index 0000000000..bd2b63b418
--- /dev/null
+++ b/src/pl/plperl/sql/plperl_call.sql
@@ -0,0 +1,36 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE plperl
+AS $$
+undef;
+$$;
+
+CALL test_proc1();
+
+
+CREATE PROCEDURE test_proc2()
+LANGUAGE plperl
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plperl
+AS $$
+spi_exec_query("INSERT INTO test1 VALUES ($_[0])");
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index d0afa59242..f459c02f7b 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -275,7 +275,6 @@ do_compile(FunctionCallInfo fcinfo,
 	bool		isnull;
 	char	   *proc_source;
 	HeapTuple	typeTup;
-	Form_pg_type typeStruct;
 	PLpgSQL_variable *var;
 	PLpgSQL_rec *rec;
 	int			i;
@@ -531,53 +530,58 @@ do_compile(FunctionCallInfo fcinfo,
 			/*
 			 * Lookup the function's return type
 			 */
-			typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rettypeid));
-			if (!HeapTupleIsValid(typeTup))
-				elog(ERROR, "cache lookup failed for type %u", rettypeid);
-			typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
-
-			/* Disallow pseudotype result, except VOID or RECORD */
-			/* (note we already replaced polymorphic types) */
-			if (typeStruct->typtype == TYPTYPE_PSEUDO)
+			if (rettypeid)
 			{
-				if (rettypeid == VOIDOID ||
-					rettypeid == RECORDOID)
-					 /* okay */ ;
-				else if (rettypeid == TRIGGEROID || rettypeid == EVTTRIGGEROID)
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("trigger functions can only be called as triggers")));
-				else
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("PL/pgSQL functions cannot return type %s",
-									format_type_be(rettypeid))));
-			}
+				Form_pg_type typeStruct;
 
-			if (typeStruct->typrelid != InvalidOid ||
-				rettypeid == RECORDOID)
-				function->fn_retistuple = true;
-			else
-			{
-				function->fn_retbyval = typeStruct->typbyval;
-				function->fn_rettyplen = typeStruct->typlen;
+				typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rettypeid));
+				if (!HeapTupleIsValid(typeTup))
+					elog(ERROR, "cache lookup failed for type %u", rettypeid);
+				typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
 
-				/*
-				 * install $0 reference, but only for polymorphic return
-				 * types, and not when the return is specified through an
-				 * output parameter.
-				 */
-				if (IsPolymorphicType(procStruct->prorettype) &&
-					num_out_args == 0)
+				/* Disallow pseudotype result, except VOID or RECORD */
+				/* (note we already replaced polymorphic types) */
+				if (typeStruct->typtype == TYPTYPE_PSEUDO)
 				{
-					(void) plpgsql_build_variable("$0", 0,
-												  build_datatype(typeTup,
-																 -1,
-																 function->fn_input_collation),
-												  true);
+					if (rettypeid == VOIDOID ||
+						rettypeid == RECORDOID)
+						/* okay */ ;
+					else if (rettypeid == TRIGGEROID || rettypeid == EVTTRIGGEROID)
+						ereport(ERROR,
+								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+								 errmsg("trigger functions can only be called as triggers")));
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+								 errmsg("PL/pgSQL functions cannot return type %s",
+										format_type_be(rettypeid))));
+				}
+
+				if (typeStruct->typrelid != InvalidOid ||
+					rettypeid == RECORDOID)
+					function->fn_retistuple = true;
+				else
+				{
+					function->fn_retbyval = typeStruct->typbyval;
+					function->fn_rettyplen = typeStruct->typlen;
+
+					/*
+					 * install $0 reference, but only for polymorphic return
+					 * types, and not when the return is specified through an
+					 * output parameter.
+					 */
+					if (IsPolymorphicType(procStruct->prorettype) &&
+						num_out_args == 0)
+					{
+						(void) plpgsql_build_variable("$0", 0,
+													  build_datatype(typeTup,
+																	 -1,
+																	 function->fn_input_collation),
+													  true);
+					}
 				}
+				ReleaseSysCache(typeTup);
 			}
-			ReleaseSysCache(typeTup);
 			break;
 
 		case PLPGSQL_DML_TRIGGER:
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 7a6dd15460..882b16e2b1 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -462,7 +462,7 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo,
 	estate.err_text = NULL;
 	estate.err_stmt = (PLpgSQL_stmt *) (func->action);
 	rc = exec_stmt_block(&estate, func->action);
-	if (rc != PLPGSQL_RC_RETURN)
+	if (rc != PLPGSQL_RC_RETURN && func->fn_rettype)
 	{
 		estate.err_stmt = NULL;
 		estate.err_text = NULL;
@@ -509,6 +509,12 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo,
 	}
 	else if (!estate.retisnull)
 	{
+		if (!func->fn_rettype)
+		{
+			ereport(ERROR,
+					(errmsg("cannot return a value from a procedure")));
+		}
+
 		if (estate.retistuple)
 		{
 			/*
diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile
index 7680d49cb6..cc91afebde 100644
--- a/src/pl/plpython/Makefile
+++ b/src/pl/plpython/Makefile
@@ -78,6 +78,7 @@ REGRESS = \
 	plpython_spi \
 	plpython_newline \
 	plpython_void \
+	plpython_call \
 	plpython_params \
 	plpython_setof \
 	plpython_record \
diff --git a/src/pl/plpython/expected/plpython_call.out b/src/pl/plpython/expected/plpython_call.out
new file mode 100644
index 0000000000..58c184e2ea
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_call.out
@@ -0,0 +1,35 @@
+--
+-- Tests for procedures / CALL syntax
+--
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpythonu
+AS $$
+pass
+$$;
+CALL test_proc1();
+-- error: can't return non-None
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpythonu
+AS $$
+return 5
+$$;
+CALL test_proc2();
+ERROR:  PL/Python procedure did not return None
+CONTEXT:  PL/Python function "test_proc2"
+CREATE TABLE test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpythonu
+AS $$
+plpy.execute("INSERT INTO test1 VALUES (%s)" % x)
+$$;
+CALL test_proc3(55);
+SELECT * FROM test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE test1;
diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
index 26f61dd0f3..1b7a9470d4 100644
--- a/src/pl/plpython/plpy_exec.c
+++ b/src/pl/plpython/plpy_exec.c
@@ -197,12 +197,19 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 		error_context_stack = &plerrcontext;
 
 		/*
-		 * If the function is declared to return void, the Python return value
+		 * For a procedure or function declared to return void, the Python return value
 		 * must be None. For void-returning functions, we also treat a None
 		 * return value as a special "void datum" rather than NULL (as is the
 		 * case for non-void-returning functions).
 		 */
-		if (proc->result.out.d.typoid == VOIDOID)
+		if (proc->is_procedure)
+		{
+			if (plrv != Py_None)
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("PL/Python procedure did not return None")));
+		}
+		else if (proc->result.out.d.typoid == VOIDOID)
 		{
 			if (plrv != Py_None)
 				ereport(ERROR,
@@ -715,7 +722,8 @@ plpython_return_error_callback(void *arg)
 {
 	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 
-	if (exec_ctx->curr_proc)
+	if (exec_ctx->curr_proc &&
+		!exec_ctx->curr_proc->is_procedure)
 		errcontext("while creating return value");
 }
 
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index 26acc88b27..7b04a60e90 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -188,6 +188,7 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 		proc->fn_tid = procTup->t_self;
 		proc->fn_readonly = (procStruct->provolatile != PROVOLATILE_VOLATILE);
 		proc->is_setof = procStruct->proretset;
+		proc->is_procedure = (procStruct->prorettype == InvalidOid);
 		PLy_typeinfo_init(&proc->result, proc->mcxt);
 		proc->src = NULL;
 		proc->argnames = NULL;
@@ -207,9 +208,9 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 
 		/*
 		 * get information required for output conversion of the return value,
-		 * but only if this isn't a trigger.
+		 * but only if this isn't a trigger or procedure.
 		 */
-		if (!is_trigger)
+		if (!is_trigger && procStruct->prorettype)
 		{
 			HeapTuple	rvTypeTup;
 			Form_pg_type rvTypeStruct;
diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h
index d05944fc39..0bb1b652b0 100644
--- a/src/pl/plpython/plpy_procedure.h
+++ b/src/pl/plpython/plpy_procedure.h
@@ -30,7 +30,8 @@ typedef struct PLyProcedure
 	TransactionId fn_xmin;
 	ItemPointerData fn_tid;
 	bool		fn_readonly;
-	bool		is_setof;		/* true, if procedure returns result set */
+	bool		is_setof;		/* true, if function returns result set */
+	bool		is_procedure;
 	PLyTypeInfo result;			/* also used to store info for trigger tuple
 								 * type */
 	char	   *src;			/* textual procedure code, after mangling */
diff --git a/src/pl/plpython/sql/plpython_call.sql b/src/pl/plpython/sql/plpython_call.sql
new file mode 100644
index 0000000000..3fb74de5f0
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_call.sql
@@ -0,0 +1,41 @@
+--
+-- Tests for procedures / CALL syntax
+--
+
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpythonu
+AS $$
+pass
+$$;
+
+CALL test_proc1();
+
+
+-- error: can't return non-None
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpythonu
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpythonu
+AS $$
+plpy.execute("INSERT INTO test1 VALUES (%s)" % x)
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/pl/tcl/Makefile b/src/pl/tcl/Makefile
index b8971d3cc8..6a92a9b6aa 100644
--- a/src/pl/tcl/Makefile
+++ b/src/pl/tcl/Makefile
@@ -28,7 +28,7 @@ DATA = pltcl.control pltcl--1.0.sql pltcl--unpackaged--1.0.sql \
        pltclu.control pltclu--1.0.sql pltclu--unpackaged--1.0.sql
 
 REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=pltcl
-REGRESS = pltcl_setup pltcl_queries pltcl_start_proc pltcl_subxact pltcl_unicode
+REGRESS = pltcl_setup pltcl_queries pltcl_call pltcl_start_proc pltcl_subxact pltcl_unicode
 
 # Tcl on win32 ships with import libraries only for Microsoft Visual C++,
 # which are not compatible with mingw gcc. Therefore we need to build a
diff --git a/src/pl/tcl/expected/pltcl_call.out b/src/pl/tcl/expected/pltcl_call.out
new file mode 100644
index 0000000000..7221a37ad0
--- /dev/null
+++ b/src/pl/tcl/expected/pltcl_call.out
@@ -0,0 +1,29 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE pltcl
+AS $$
+unset
+$$;
+CALL test_proc1();
+CREATE PROCEDURE test_proc2()
+LANGUAGE pltcl
+AS $$
+return 5
+$$;
+CALL test_proc2();
+CREATE TABLE test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE pltcl
+AS $$
+spi_exec "INSERT INTO test1 VALUES ($1)"
+$$;
+CALL test_proc3(55);
+SELECT * FROM test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE test1;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index 6d97ddc99b..e0792d93e1 100644
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -146,6 +146,7 @@ typedef struct pltcl_proc_desc
 	Oid			result_typid;	/* OID of fn's result type */
 	FmgrInfo	result_in_func; /* input function for fn's result type */
 	Oid			result_typioparam;	/* param to pass to same */
+	bool		fn_is_procedure;/* true if this is a procedure */
 	bool		fn_retisset;	/* true if function returns a set */
 	bool		fn_retistuple;	/* true if function returns composite */
 	bool		fn_retisdomain; /* true if function returns domain */
@@ -968,7 +969,7 @@ pltcl_func_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
 		retval = (Datum) 0;
 		fcinfo->isnull = true;
 	}
-	else if (fcinfo->isnull)
+	else if (fcinfo->isnull && !prodesc->fn_is_procedure)
 	{
 		retval = InputFunctionCall(&prodesc->result_in_func,
 								   NULL,
@@ -1026,11 +1027,13 @@ pltcl_func_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
 									   call_state);
 		retval = HeapTupleGetDatum(tup);
 	}
-	else
+	else if (!prodesc->fn_is_procedure)
 		retval = InputFunctionCall(&prodesc->result_in_func,
 								   utf_u2e(Tcl_GetStringResult(interp)),
 								   prodesc->result_typioparam,
 								   -1);
+	else
+		retval = 0;
 
 	return retval;
 }
@@ -1506,7 +1509,9 @@ compile_pltcl_function(Oid fn_oid, Oid tgreloid,
 		 * Get the required information for input conversion of the
 		 * return value.
 		 ************************************************************/
-		if (!is_trigger && !is_event_trigger)
+		prodesc->fn_is_procedure = (procStruct->prorettype == InvalidOid);
+
+		if (!is_trigger && !is_event_trigger && procStruct->prorettype)
 		{
 			Oid			rettype = procStruct->prorettype;
 
@@ -2199,7 +2204,7 @@ pltcl_returnnext(ClientData cdata, Tcl_Interp *interp,
 				tuplestore_puttuple(call_state->tuple_store, tuple);
 			}
 		}
-		else
+		else if (!prodesc->fn_is_procedure)
 		{
 			Datum		retval;
 			bool		isNull = false;
diff --git a/src/pl/tcl/sql/pltcl_call.sql b/src/pl/tcl/sql/pltcl_call.sql
new file mode 100644
index 0000000000..ef1f540f50
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_call.sql
@@ -0,0 +1,36 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE pltcl
+AS $$
+unset
+$$;
+
+CALL test_proc1();
+
+
+CREATE PROCEDURE test_proc2()
+LANGUAGE pltcl
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE pltcl
+AS $$
+spi_exec "INSERT INTO test1 VALUES ($1)"
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/test/regress/expected/create_procedure.out b/src/test/regress/expected/create_procedure.out
new file mode 100644
index 0000000000..eeb129d71f
--- /dev/null
+++ b/src/test/regress/expected/create_procedure.out
@@ -0,0 +1,86 @@
+CALL nonexistent();  -- error
+ERROR:  function nonexistent() does not exist
+LINE 1: CALL nonexistent();
+             ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+CALL random();  -- error
+ERROR:  random() is not a procedure
+LINE 1: CALL random();
+             ^
+HINT:  To call a function, use SELECT.
+CREATE FUNCTION testfunc1(a int) RETURNS int LANGUAGE SQL AS $$ SELECT a $$;
+CREATE TABLE cp_test (a int, b text);
+CREATE PROCEDURE ptest1(x text)
+LANGUAGE SQL
+AS $$
+INSERT INTO cp_test VALUES (1, x);
+$$;
+SELECT ptest1('x');  -- error
+ERROR:  ptest1(unknown) is a procedure
+LINE 1: SELECT ptest1('x');
+               ^
+HINT:  To call a procedure, use CALL.
+CALL ptest1('a');  -- ok
+\df ptest1
+                        List of functions
+ Schema |  Name  | Result data type | Argument data types | Type 
+--------+--------+------------------+---------------------+------
+ public | ptest1 |                  | x text              | proc
+(1 row)
+
+SELECT * FROM cp_test ORDER BY a;
+ a | b 
+---+---
+ 1 | a
+(1 row)
+
+CREATE PROCEDURE ptest2()
+LANGUAGE SQL
+AS $$
+SELECT 5;
+$$;
+CALL ptest2();
+-- various error cases
+CREATE PROCEDURE ptestx() LANGUAGE SQL WINDOW AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+ERROR:  procedure cannot have WINDOW attribute
+CREATE PROCEDURE ptestx() LANGUAGE SQL STRICT AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+ERROR:  procedure cannot be declared as strict
+CREATE PROCEDURE ptestx(OUT a int) LANGUAGE SQL AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+ERROR:  procedures cannot have OUT parameters
+ALTER PROCEDURE ptest1(text) STRICT;
+ERROR:  procedure strictness cannot be changed
+ALTER FUNCTION ptest1(text) VOLATILE;  -- error: not a function
+ERROR:  ptest1(text) is not a function
+ALTER PROCEDURE testfunc1(int) VOLATILE;  -- error: not a procedure
+ERROR:  testfunc1(integer) is not a procedure
+ALTER PROCEDURE nonexistent() VOLATILE;
+ERROR:  procedure nonexistent() does not exist
+DROP FUNCTION ptest1(text);  -- error: not a function
+ERROR:  ptest1(text) is not a function
+DROP PROCEDURE testfunc1(int);  -- error: not a procedure
+ERROR:  testfunc1(integer) is not a procedure
+DROP PROCEDURE nonexistent();
+ERROR:  procedure nonexistent() does not exist
+-- privileges
+CREATE USER regress_user1;
+GRANT INSERT ON cp_test TO regress_user1;
+REVOKE EXECUTE ON PROCEDURE ptest1(text) FROM PUBLIC;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- error
+ERROR:  permission denied for function ptest1
+RESET ROLE;
+GRANT EXECUTE ON PROCEDURE ptest1(text) TO regress_user1;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- ok
+RESET ROLE;
+-- ROUTINE syntax
+ALTER ROUTINE testfunc1(int) RENAME TO testfunc1a;
+ALTER ROUTINE testfunc1a RENAME TO testfunc1;
+ALTER ROUTINE ptest1(text) RENAME TO ptest1a;
+ALTER ROUTINE ptest1a RENAME TO ptest1;
+DROP ROUTINE testfunc1(int);
+-- cleanup
+DROP PROCEDURE ptest1;
+DROP PROCEDURE ptest2;
+DROP TABLE cp_test;
+DROP USER regress_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 1fdadbc9ef..bfd9d54c11 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -29,6 +29,7 @@ CREATE DOMAIN addr_nsp.gendomain AS int4 CONSTRAINT domconstr CHECK (value > 0);
 CREATE FUNCTION addr_nsp.trig() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN END; $$;
 CREATE TRIGGER t BEFORE INSERT ON addr_nsp.gentable FOR EACH ROW EXECUTE PROCEDURE addr_nsp.trig();
 CREATE POLICY genpol ON addr_nsp.gentable;
+CREATE PROCEDURE addr_nsp.proc(int4) LANGUAGE SQL AS $$ $$;
 CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
@@ -88,7 +89,7 @@ BEGIN
 		('table'), ('index'), ('sequence'), ('view'),
 		('materialized view'), ('foreign table'),
 		('table column'), ('foreign table column'),
-		('aggregate'), ('function'), ('type'), ('cast'),
+		('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
 		('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
 		('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
 		('text search parser'), ('text search dictionary'),
@@ -171,6 +172,12 @@ WARNING:  error for function,{addr_nsp,zwei},{}: function addr_nsp.zwei() does n
 WARNING:  error for function,{addr_nsp,zwei},{integer}: function addr_nsp.zwei(integer) does not exist
 WARNING:  error for function,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for function,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for procedure,{eins},{}: procedure eins() does not exist
+WARNING:  error for procedure,{eins},{integer}: procedure eins(integer) does not exist
+WARNING:  error for procedure,{addr_nsp,zwei},{}: procedure addr_nsp.zwei() does not exist
+WARNING:  error for procedure,{addr_nsp,zwei},{integer}: procedure addr_nsp.zwei(integer) does not exist
+WARNING:  error for procedure,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for procedure,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for type,{eins},{}: type "eins" does not exist
 WARNING:  error for type,{eins},{integer}: type "eins" does not exist
 WARNING:  error for type,{addr_nsp,zwei},{}: name list length must be exactly 1
@@ -371,6 +378,7 @@ WITH objects (type, name, args) AS (VALUES
 				('foreign table column', '{addr_nsp, genftable, a}', '{}'),
 				('aggregate', '{addr_nsp, genaggr}', '{int4}'),
 				('function', '{pg_catalog, pg_identify_object}', '{pg_catalog.oid, pg_catalog.oid, int4}'),
+				('procedure', '{addr_nsp, proc}', '{int4}'),
 				('type', '{pg_catalog._int4}', '{}'),
 				('type', '{addr_nsp.gendomain}', '{}'),
 				('type', '{addr_nsp.gencomptype}', '{}'),
@@ -431,6 +439,7 @@ SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*,
  type                      | addr_nsp   | gendomain         | addr_nsp.gendomain                                                   | t
  function                  | pg_catalog |                   | pg_catalog.pg_identify_object(pg_catalog.oid,pg_catalog.oid,integer) | t
  aggregate                 | addr_nsp   |                   | addr_nsp.genaggr(integer)                                            | t
+ procedure                 | addr_nsp   |                   | addr_nsp.proc(integer)                                               | t
  sequence                  | addr_nsp   | gentable_a_seq    | addr_nsp.gentable_a_seq                                              | t
  table                     | addr_nsp   | gentable          | addr_nsp.gentable                                                    | t
  table column              | addr_nsp   | gentable          | addr_nsp.gentable.b                                                  | t
@@ -469,7 +478,7 @@ SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*,
  subscription              |            | addr_sub          | addr_sub                                                             | t
  publication               |            | addr_pub          | addr_pub                                                             | t
  publication relation      |            |                   | gentable in publication addr_pub                                     | t
-(46 rows)
+(47 rows)
 
 ---
 --- Cleanup resources
@@ -480,6 +489,6 @@ NOTICE:  drop cascades to 4 other objects
 DROP PUBLICATION addr_pub;
 DROP SUBSCRIPTION addr_sub;
 DROP SCHEMA addr_nsp CASCADE;
-NOTICE:  drop cascades to 12 other objects
+NOTICE:  drop cascades to 13 other objects
 DROP OWNED BY regress_addr_user;
 DROP USER regress_addr_user;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index bb3532676b..d6e5bc3353 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -6040,3 +6040,44 @@ END; $$ LANGUAGE plpgsql;
 ERROR:  "x" is not a scalar variable
 LINE 3:   GET DIAGNOSTICS x = ROW_COUNT;
                           ^
+--
+-- Procedures
+--
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    NULL;
+END;
+$$;
+CALL test_proc1();
+-- error: can't return non-NULL
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN 5;
+END;
+$$;
+CALL test_proc2();
+ERROR:  cannot return a value from a procedure
+CONTEXT:  PL/pgSQL function test_proc2() while casting return value to function's return type
+CREATE TABLE proc_test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    INSERT INTO proc_test1 VALUES (x);
+END;
+$$;
+CALL test_proc3(55);
+SELECT * FROM proc_test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE proc_test1;
diff --git a/src/test/regress/expected/polymorphism.out b/src/test/regress/expected/polymorphism.out
index 91cfb743b6..66e35a6a5c 100644
--- a/src/test/regress/expected/polymorphism.out
+++ b/src/test/regress/expected/polymorphism.out
@@ -915,10 +915,10 @@ select dfunc();
 
 -- verify it lists properly
 \df dfunc
-                                           List of functions
- Schema | Name  | Result data type |                    Argument data types                    |  Type  
---------+-------+------------------+-----------------------------------------------------------+--------
- public | dfunc | integer          | a integer DEFAULT 1, OUT sum integer, b integer DEFAULT 2 | normal
+                                          List of functions
+ Schema | Name  | Result data type |                    Argument data types                    | Type 
+--------+-------+------------------+-----------------------------------------------------------+------
+ public | dfunc | integer          | a integer DEFAULT 1, OUT sum integer, b integer DEFAULT 2 | func
 (1 row)
 
 drop function dfunc(int, int);
@@ -1083,10 +1083,10 @@ $$ select array_upper($1, 1) $$ language sql;
 ERROR:  cannot remove parameter defaults from existing function
 HINT:  Use DROP FUNCTION dfunc(integer[]) first.
 \df dfunc
-                                      List of functions
- Schema | Name  | Result data type |               Argument data types               |  Type  
---------+-------+------------------+-------------------------------------------------+--------
- public | dfunc | integer          | VARIADIC a integer[] DEFAULT ARRAY[]::integer[] | normal
+                                     List of functions
+ Schema | Name  | Result data type |               Argument data types               | Type 
+--------+-------+------------------+-------------------------------------------------+------
+ public | dfunc | integer          | VARIADIC a integer[] DEFAULT ARRAY[]::integer[] | func
 (1 row)
 
 drop function dfunc(a variadic int[]);
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 771971a095..e6994f0490 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -651,13 +651,25 @@ GRANT USAGE ON LANGUAGE sql TO regress_user2; -- fail
 WARNING:  no privileges were granted for "sql"
 CREATE FUNCTION testfunc1(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql;
 CREATE FUNCTION testfunc2(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
-REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int) FROM PUBLIC;
-GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int) TO regress_user2;
+CREATE AGGREGATE testagg1(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testproc1(int) AS 'select $1;' LANGUAGE sql;
+REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) TO regress_user2;
+REVOKE ALL ON FUNCTION testproc1(int) FROM PUBLIC; -- fail, not a function
+ERROR:  testproc1(integer) is not a function
+REVOKE ALL ON PROCEDURE testproc1(int) FROM PUBLIC;
+GRANT EXECUTE ON PROCEDURE testproc1(int) TO regress_user2;
 GRANT USAGE ON FUNCTION testfunc1(int) TO regress_user3; -- semantic error
 ERROR:  invalid privilege type USAGE for function
+GRANT USAGE ON FUNCTION testagg1(int) TO regress_user3; -- semantic error
+ERROR:  invalid privilege type USAGE for function
+GRANT USAGE ON PROCEDURE testproc1(int) TO regress_user3; -- semantic error
+ERROR:  invalid privilege type USAGE for procedure
 GRANT ALL PRIVILEGES ON FUNCTION testfunc1(int) TO regress_user4;
 GRANT ALL PRIVILEGES ON FUNCTION testfunc_nosuch(int) TO regress_user4;
 ERROR:  function testfunc_nosuch(integer) does not exist
+GRANT ALL PRIVILEGES ON FUNCTION testagg1(int) TO regress_user4;
+GRANT ALL PRIVILEGES ON PROCEDURE testproc1(int) TO regress_user4;
 CREATE FUNCTION testfunc4(boolean) RETURNS text
   AS 'select col1 from atest2 where col2 = $1;'
   LANGUAGE sql SECURITY DEFINER;
@@ -671,9 +683,20 @@ SELECT testfunc1(5), testfunc2(5); -- ok
 
 CREATE FUNCTION testfunc3(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql; -- fail
 ERROR:  permission denied for language sql
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
+ testagg1 
+----------
+        6
+(1 row)
+
+CALL testproc1(6); -- ok
 SET SESSION AUTHORIZATION regress_user3;
 SELECT testfunc1(5); -- fail
 ERROR:  permission denied for function testfunc1
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- fail
+ERROR:  permission denied for function testagg1
+CALL testproc1(6); -- fail
+ERROR:  permission denied for function testproc1
 SELECT col1 FROM atest2 WHERE col2 = true; -- fail
 ERROR:  permission denied for relation atest2
 SELECT testfunc4(true); -- ok
@@ -689,8 +712,19 @@ SELECT testfunc1(5); -- ok
         10
 (1 row)
 
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
+ testagg1 
+----------
+        6
+(1 row)
+
+CALL testproc1(6); -- ok
 DROP FUNCTION testfunc1(int); -- fail
 ERROR:  must be owner of function testfunc1
+DROP AGGREGATE testagg1(int); -- fail
+ERROR:  must be owner of function testagg1
+DROP PROCEDURE testproc1(int); -- fail
+ERROR:  must be owner of function testproc1
 \c -
 DROP FUNCTION testfunc1(int); -- ok
 -- restore to sanity
@@ -1537,22 +1571,54 @@ SELECT has_schema_privilege('regress_user2', 'testns5', 'CREATE'); -- no
 
 SET ROLE regress_user1;
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testns.bar() AS 'select 1' LANGUAGE sql;
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- no
  has_function_privilege 
 ------------------------
  f
 (1 row)
 
-ALTER DEFAULT PRIVILEGES IN SCHEMA testns GRANT EXECUTE ON FUNCTIONS to public;
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- no
+ has_function_privilege 
+------------------------
+ f
+(1 row)
+
+SELECT has_function_privilege('regress_user2', 'testns.bar()', 'EXECUTE'); -- no
+ has_function_privilege 
+------------------------
+ f
+(1 row)
+
+ALTER DEFAULT PRIVILEGES IN SCHEMA testns GRANT EXECUTE ON ROUTINES to public;
 DROP FUNCTION testns.foo();
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+DROP AGGREGATE testns.agg1(int);
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
+DROP PROCEDURE testns.bar();
+CREATE PROCEDURE testns.bar() AS 'select 1' LANGUAGE sql;
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- yes
  has_function_privilege 
 ------------------------
  t
 (1 row)
 
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- yes
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
+SELECT has_function_privilege('regress_user2', 'testns.bar()', 'EXECUTE'); -- yes (counts as function here)
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
 DROP FUNCTION testns.foo();
+DROP AGGREGATE testns.agg1(int);
+DROP PROCEDURE testns.bar();
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_user1 REVOKE USAGE ON TYPES FROM public;
 CREATE DOMAIN testns.testdomain1 AS int;
 SELECT has_type_privilege('regress_user2', 'testns.testdomain1', 'USAGE'); -- no
@@ -1631,12 +1697,26 @@ SELECT has_table_privilege('regress_user1', 'testns.t2', 'SELECT'); -- false
 (1 row)
 
 CREATE FUNCTION testns.testfunc(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
+CREATE AGGREGATE testns.testagg(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testns.testproc(int) AS 'select 3' LANGUAGE sql;
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- true by default
  has_function_privilege 
 ------------------------
  t
 (1 row)
 
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- true by default
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- true by default
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
 REVOKE ALL ON ALL FUNCTIONS IN SCHEMA testns FROM PUBLIC;
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- false
  has_function_privilege 
@@ -1644,9 +1724,47 @@ SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'
  f
 (1 row)
 
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- false
+ has_function_privilege 
+------------------------
+ f
+(1 row)
+
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- still true, not a function
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
+REVOKE ALL ON ALL PROCEDURES IN SCHEMA testns FROM PUBLIC;
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- now false
+ has_function_privilege 
+------------------------
+ f
+(1 row)
+
+GRANT ALL ON ALL ROUTINES IN SCHEMA testns TO PUBLIC;
+SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- true
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- true
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- true
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
 \set VERBOSITY terse \\ -- suppress cascade details
 DROP SCHEMA testns CASCADE;
-NOTICE:  drop cascades to 3 other objects
+NOTICE:  drop cascades to 5 other objects
 \set VERBOSITY default
 -- Change owner of the schema & and rename of new schema owner
 \c -
@@ -1729,8 +1847,10 @@ drop table dep_priv_test;
 -- clean up
 \c
 drop sequence x_seq;
+DROP AGGREGATE testagg1(int);
 DROP FUNCTION testfunc2(int);
 DROP FUNCTION testfunc4(boolean);
+DROP PROCEDURE testproc1(int);
 DROP VIEW atestv0;
 DROP VIEW atestv1;
 DROP VIEW atestv2;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index aa5e6af621..9343aa02c7 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -53,7 +53,7 @@ test: copy copyselect copydml
 # ----------
 # More groups of parallel tests
 # ----------
-test: create_misc create_operator
+test: create_misc create_operator create_procedure
 # These depend on the above two
 test: create_index create_view
 
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 3866314a92..8dd4b7ba0c 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -63,6 +63,7 @@ test: copyselect
 test: copydml
 test: create_misc
 test: create_operator
+test: create_procedure
 test: create_index
 test: create_view
 test: create_aggregate
diff --git a/src/test/regress/sql/create_procedure.sql b/src/test/regress/sql/create_procedure.sql
new file mode 100644
index 0000000000..f09ba2ad30
--- /dev/null
+++ b/src/test/regress/sql/create_procedure.sql
@@ -0,0 +1,79 @@
+CALL nonexistent();  -- error
+CALL random();  -- error
+
+CREATE FUNCTION testfunc1(a int) RETURNS int LANGUAGE SQL AS $$ SELECT a $$;
+
+CREATE TABLE cp_test (a int, b text);
+
+CREATE PROCEDURE ptest1(x text)
+LANGUAGE SQL
+AS $$
+INSERT INTO cp_test VALUES (1, x);
+$$;
+
+SELECT ptest1('x');  -- error
+CALL ptest1('a');  -- ok
+
+\df ptest1
+
+SELECT * FROM cp_test ORDER BY a;
+
+
+CREATE PROCEDURE ptest2()
+LANGUAGE SQL
+AS $$
+SELECT 5;
+$$;
+
+CALL ptest2();
+
+
+-- various error cases
+
+CREATE PROCEDURE ptestx() LANGUAGE SQL WINDOW AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+CREATE PROCEDURE ptestx() LANGUAGE SQL STRICT AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+CREATE PROCEDURE ptestx(OUT a int) LANGUAGE SQL AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+
+ALTER PROCEDURE ptest1(text) STRICT;
+ALTER FUNCTION ptest1(text) VOLATILE;  -- error: not a function
+ALTER PROCEDURE testfunc1(int) VOLATILE;  -- error: not a procedure
+ALTER PROCEDURE nonexistent() VOLATILE;
+
+DROP FUNCTION ptest1(text);  -- error: not a function
+DROP PROCEDURE testfunc1(int);  -- error: not a procedure
+DROP PROCEDURE nonexistent();
+
+
+-- privileges
+
+CREATE USER regress_user1;
+GRANT INSERT ON cp_test TO regress_user1;
+REVOKE EXECUTE ON PROCEDURE ptest1(text) FROM PUBLIC;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- error
+RESET ROLE;
+GRANT EXECUTE ON PROCEDURE ptest1(text) TO regress_user1;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- ok
+RESET ROLE;
+
+
+-- ROUTINE syntax
+
+ALTER ROUTINE testfunc1(int) RENAME TO testfunc1a;
+ALTER ROUTINE testfunc1a RENAME TO testfunc1;
+
+ALTER ROUTINE ptest1(text) RENAME TO ptest1a;
+ALTER ROUTINE ptest1a RENAME TO ptest1;
+
+DROP ROUTINE testfunc1(int);
+
+
+-- cleanup
+
+DROP PROCEDURE ptest1;
+DROP PROCEDURE ptest2;
+
+DROP TABLE cp_test;
+
+DROP USER regress_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 63821b8008..55faa71edf 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -32,6 +32,7 @@ CREATE DOMAIN addr_nsp.gendomain AS int4 CONSTRAINT domconstr CHECK (value > 0);
 CREATE FUNCTION addr_nsp.trig() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN END; $$;
 CREATE TRIGGER t BEFORE INSERT ON addr_nsp.gentable FOR EACH ROW EXECUTE PROCEDURE addr_nsp.trig();
 CREATE POLICY genpol ON addr_nsp.gentable;
+CREATE PROCEDURE addr_nsp.proc(int4) LANGUAGE SQL AS $$ $$;
 CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
@@ -81,7 +82,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 		('table'), ('index'), ('sequence'), ('view'),
 		('materialized view'), ('foreign table'),
 		('table column'), ('foreign table column'),
-		('aggregate'), ('function'), ('type'), ('cast'),
+		('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
 		('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
 		('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
 		('text search parser'), ('text search dictionary'),
@@ -147,6 +148,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 				('foreign table column', '{addr_nsp, genftable, a}', '{}'),
 				('aggregate', '{addr_nsp, genaggr}', '{int4}'),
 				('function', '{pg_catalog, pg_identify_object}', '{pg_catalog.oid, pg_catalog.oid, int4}'),
+				('procedure', '{addr_nsp, proc}', '{int4}'),
 				('type', '{pg_catalog._int4}', '{}'),
 				('type', '{addr_nsp.gendomain}', '{}'),
 				('type', '{addr_nsp.gencomptype}', '{}'),
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index 6620ea6172..1c355132b7 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -4820,3 +4820,52 @@ CREATE FUNCTION fx(x WSlot) RETURNS void AS $$
   GET DIAGNOSTICS x = ROW_COUNT;
   RETURN;
 END; $$ LANGUAGE plpgsql;
+
+
+--
+-- Procedures
+--
+
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    NULL;
+END;
+$$;
+
+CALL test_proc1();
+
+
+-- error: can't return non-NULL
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN 5;
+END;
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE proc_test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    INSERT INTO proc_test1 VALUES (x);
+END;
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM proc_test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE proc_test1;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index a900ba2f84..ea8dd028cd 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -442,12 +442,21 @@ CREATE TABLE atestc (fz int) INHERITS (atestp1, atestp2);
 GRANT USAGE ON LANGUAGE sql TO regress_user2; -- fail
 CREATE FUNCTION testfunc1(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql;
 CREATE FUNCTION testfunc2(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
-
-REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int) FROM PUBLIC;
-GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int) TO regress_user2;
+CREATE AGGREGATE testagg1(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testproc1(int) AS 'select $1;' LANGUAGE sql;
+
+REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) TO regress_user2;
+REVOKE ALL ON FUNCTION testproc1(int) FROM PUBLIC; -- fail, not a function
+REVOKE ALL ON PROCEDURE testproc1(int) FROM PUBLIC;
+GRANT EXECUTE ON PROCEDURE testproc1(int) TO regress_user2;
 GRANT USAGE ON FUNCTION testfunc1(int) TO regress_user3; -- semantic error
+GRANT USAGE ON FUNCTION testagg1(int) TO regress_user3; -- semantic error
+GRANT USAGE ON PROCEDURE testproc1(int) TO regress_user3; -- semantic error
 GRANT ALL PRIVILEGES ON FUNCTION testfunc1(int) TO regress_user4;
 GRANT ALL PRIVILEGES ON FUNCTION testfunc_nosuch(int) TO regress_user4;
+GRANT ALL PRIVILEGES ON FUNCTION testagg1(int) TO regress_user4;
+GRANT ALL PRIVILEGES ON PROCEDURE testproc1(int) TO regress_user4;
 
 CREATE FUNCTION testfunc4(boolean) RETURNS text
   AS 'select col1 from atest2 where col2 = $1;'
@@ -457,16 +466,24 @@ CREATE FUNCTION testfunc4(boolean) RETURNS text
 SET SESSION AUTHORIZATION regress_user2;
 SELECT testfunc1(5), testfunc2(5); -- ok
 CREATE FUNCTION testfunc3(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql; -- fail
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
+CALL testproc1(6); -- ok
 
 SET SESSION AUTHORIZATION regress_user3;
 SELECT testfunc1(5); -- fail
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- fail
+CALL testproc1(6); -- fail
 SELECT col1 FROM atest2 WHERE col2 = true; -- fail
 SELECT testfunc4(true); -- ok
 
 SET SESSION AUTHORIZATION regress_user4;
 SELECT testfunc1(5); -- ok
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
+CALL testproc1(6); -- ok
 
 DROP FUNCTION testfunc1(int); -- fail
+DROP AGGREGATE testagg1(int); -- fail
+DROP PROCEDURE testproc1(int); -- fail
 
 \c -
 
@@ -931,17 +948,29 @@ CREATE SCHEMA testns5;
 SET ROLE regress_user1;
 
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testns.bar() AS 'select 1' LANGUAGE sql;
 
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- no
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- no
+SELECT has_function_privilege('regress_user2', 'testns.bar()', 'EXECUTE'); -- no
 
-ALTER DEFAULT PRIVILEGES IN SCHEMA testns GRANT EXECUTE ON FUNCTIONS to public;
+ALTER DEFAULT PRIVILEGES IN SCHEMA testns GRANT EXECUTE ON ROUTINES to public;
 
 DROP FUNCTION testns.foo();
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+DROP AGGREGATE testns.agg1(int);
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
+DROP PROCEDURE testns.bar();
+CREATE PROCEDURE testns.bar() AS 'select 1' LANGUAGE sql;
 
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- yes
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- yes
+SELECT has_function_privilege('regress_user2', 'testns.bar()', 'EXECUTE'); -- yes (counts as function here)
 
 DROP FUNCTION testns.foo();
+DROP AGGREGATE testns.agg1(int);
+DROP PROCEDURE testns.bar();
 
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_user1 REVOKE USAGE ON TYPES FROM public;
 
@@ -995,12 +1024,28 @@ CREATE TABLE testns.t2 (f1 int);
 SELECT has_table_privilege('regress_user1', 'testns.t2', 'SELECT'); -- false
 
 CREATE FUNCTION testns.testfunc(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
+CREATE AGGREGATE testns.testagg(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testns.testproc(int) AS 'select 3' LANGUAGE sql;
 
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- true by default
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- true by default
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- true by default
 
 REVOKE ALL ON ALL FUNCTIONS IN SCHEMA testns FROM PUBLIC;
 
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- false
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- false
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- still true, not a function
+
+REVOKE ALL ON ALL PROCEDURES IN SCHEMA testns FROM PUBLIC;
+
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- now false
+
+GRANT ALL ON ALL ROUTINES IN SCHEMA testns TO PUBLIC;
+
+SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- true
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- true
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- true
 
 \set VERBOSITY terse \\ -- suppress cascade details
 DROP SCHEMA testns CASCADE;
@@ -1064,8 +1109,10 @@ CREATE SCHEMA testns;
 
 drop sequence x_seq;
 
+DROP AGGREGATE testagg1(int);
 DROP FUNCTION testfunc2(int);
 DROP FUNCTION testfunc4(boolean);
+DROP PROCEDURE testproc1(int);
 
 DROP VIEW atestv0;
 DROP VIEW atestv1;

base-commit: 591c504fad0de88b559bf28e929d23672179a857
-- 
2.15.0

0001-Add-tests-for-privileges-on-aggregate-functions.patchtext/plain; charset=UTF-8; name=0001-Add-tests-for-privileges-on-aggregate-functions.patch; x-mac-creator=0; x-mac-type=0Download
From aae07883d89de72a76efc49ce7239bc83a3ef95f Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Thu, 9 Nov 2017 13:25:17 -0500
Subject: [PATCH] Add tests for privileges on aggregate functions

---
 src/test/regress/expected/privileges.out | 56 ++++++++++++++++++++++++++++++--
 src/test/regress/sql/privileges.sql      | 21 ++++++++++--
 2 files changed, 72 insertions(+), 5 deletions(-)

diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 65d950f15b..fe43f464d7 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -651,13 +651,17 @@ GRANT USAGE ON LANGUAGE sql TO regress_user2; -- fail
 WARNING:  no privileges were granted for "sql"
 CREATE FUNCTION testfunc1(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql;
 CREATE FUNCTION testfunc2(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
-REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int) FROM PUBLIC;
-GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int) TO regress_user2;
+CREATE AGGREGATE testagg1(int) (sfunc = int4pl, stype = int4);
+REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) TO regress_user2;
 GRANT USAGE ON FUNCTION testfunc1(int) TO regress_user3; -- semantic error
 ERROR:  invalid privilege type USAGE for function
+GRANT USAGE ON FUNCTION testagg1(int) TO regress_user3; -- semantic error
+ERROR:  invalid privilege type USAGE for function
 GRANT ALL PRIVILEGES ON FUNCTION testfunc1(int) TO regress_user4;
 GRANT ALL PRIVILEGES ON FUNCTION testfunc_nosuch(int) TO regress_user4;
 ERROR:  function testfunc_nosuch(integer) does not exist
+GRANT ALL PRIVILEGES ON FUNCTION testagg1(int) TO regress_user4;
 CREATE FUNCTION testfunc4(boolean) RETURNS text
   AS 'select col1 from atest2 where col2 = $1;'
   LANGUAGE sql SECURITY DEFINER;
@@ -671,9 +675,17 @@ SELECT testfunc1(5), testfunc2(5); -- ok
 
 CREATE FUNCTION testfunc3(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql; -- fail
 ERROR:  permission denied for language sql
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
+ testagg1 
+----------
+        6
+(1 row)
+
 SET SESSION AUTHORIZATION regress_user3;
 SELECT testfunc1(5); -- fail
 ERROR:  permission denied for function testfunc1
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- fail
+ERROR:  permission denied for function testagg1
 SELECT col1 FROM atest2 WHERE col2 = true; -- fail
 ERROR:  permission denied for relation atest2
 SELECT testfunc4(true); -- ok
@@ -689,8 +701,16 @@ SELECT testfunc1(5); -- ok
         10
 (1 row)
 
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
+ testagg1 
+----------
+        6
+(1 row)
+
 DROP FUNCTION testfunc1(int); -- fail
 ERROR:  must be owner of function testfunc1
+DROP AGGREGATE testagg1(int); -- fail
+ERROR:  must be owner of function testagg1
 \c -
 DROP FUNCTION testfunc1(int); -- ok
 -- restore to sanity
@@ -1535,22 +1555,38 @@ SELECT has_schema_privilege('regress_user2', 'testns5', 'CREATE'); -- no
 
 SET ROLE regress_user1;
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- no
  has_function_privilege 
 ------------------------
  f
 (1 row)
 
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- no
+ has_function_privilege 
+------------------------
+ f
+(1 row)
+
 ALTER DEFAULT PRIVILEGES IN SCHEMA testns GRANT EXECUTE ON FUNCTIONS to public;
 DROP FUNCTION testns.foo();
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+DROP AGGREGATE testns.agg1(int);
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- yes
  has_function_privilege 
 ------------------------
  t
 (1 row)
 
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- yes
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
 DROP FUNCTION testns.foo();
+DROP AGGREGATE testns.agg1(int);
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_user1 REVOKE USAGE ON TYPES FROM public;
 CREATE DOMAIN testns.testdomain1 AS int;
 SELECT has_type_privilege('regress_user2', 'testns.testdomain1', 'USAGE'); -- no
@@ -1629,12 +1665,19 @@ SELECT has_table_privilege('regress_user1', 'testns.t2', 'SELECT'); -- false
 (1 row)
 
 CREATE FUNCTION testns.testfunc(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
+CREATE AGGREGATE testns.testagg(int) (sfunc = int4pl, stype = int4);
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- true by default
  has_function_privilege 
 ------------------------
  t
 (1 row)
 
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- true by default
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
 REVOKE ALL ON ALL FUNCTIONS IN SCHEMA testns FROM PUBLIC;
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- false
  has_function_privilege 
@@ -1642,9 +1685,15 @@ SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'
  f
 (1 row)
 
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- false
+ has_function_privilege 
+------------------------
+ f
+(1 row)
+
 \set VERBOSITY terse \\ -- suppress cascade details
 DROP SCHEMA testns CASCADE;
-NOTICE:  drop cascades to 3 other objects
+NOTICE:  drop cascades to 4 other objects
 \set VERBOSITY default
 -- Change owner of the schema & and rename of new schema owner
 \c -
@@ -1727,6 +1776,7 @@ drop table dep_priv_test;
 -- clean up
 \c
 drop sequence x_seq;
+DROP AGGREGATE testagg1(int);
 DROP FUNCTION testfunc2(int);
 DROP FUNCTION testfunc4(boolean);
 DROP VIEW atestv0;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 902f64c747..d7761994b6 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -442,12 +442,15 @@ CREATE TABLE atestc (fz int) INHERITS (atestp1, atestp2);
 GRANT USAGE ON LANGUAGE sql TO regress_user2; -- fail
 CREATE FUNCTION testfunc1(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql;
 CREATE FUNCTION testfunc2(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
+CREATE AGGREGATE testagg1(int) (sfunc = int4pl, stype = int4);
 
-REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int) FROM PUBLIC;
-GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int) TO regress_user2;
+REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) TO regress_user2;
 GRANT USAGE ON FUNCTION testfunc1(int) TO regress_user3; -- semantic error
+GRANT USAGE ON FUNCTION testagg1(int) TO regress_user3; -- semantic error
 GRANT ALL PRIVILEGES ON FUNCTION testfunc1(int) TO regress_user4;
 GRANT ALL PRIVILEGES ON FUNCTION testfunc_nosuch(int) TO regress_user4;
+GRANT ALL PRIVILEGES ON FUNCTION testagg1(int) TO regress_user4;
 
 CREATE FUNCTION testfunc4(boolean) RETURNS text
   AS 'select col1 from atest2 where col2 = $1;'
@@ -457,16 +460,20 @@ CREATE FUNCTION testfunc4(boolean) RETURNS text
 SET SESSION AUTHORIZATION regress_user2;
 SELECT testfunc1(5), testfunc2(5); -- ok
 CREATE FUNCTION testfunc3(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql; -- fail
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
 
 SET SESSION AUTHORIZATION regress_user3;
 SELECT testfunc1(5); -- fail
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- fail
 SELECT col1 FROM atest2 WHERE col2 = true; -- fail
 SELECT testfunc4(true); -- ok
 
 SET SESSION AUTHORIZATION regress_user4;
 SELECT testfunc1(5); -- ok
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
 
 DROP FUNCTION testfunc1(int); -- fail
+DROP AGGREGATE testagg1(int); -- fail
 
 \c -
 
@@ -929,17 +936,23 @@ CREATE SCHEMA testns5;
 SET ROLE regress_user1;
 
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
 
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- no
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- no
 
 ALTER DEFAULT PRIVILEGES IN SCHEMA testns GRANT EXECUTE ON FUNCTIONS to public;
 
 DROP FUNCTION testns.foo();
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+DROP AGGREGATE testns.agg1(int);
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
 
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- yes
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- yes
 
 DROP FUNCTION testns.foo();
+DROP AGGREGATE testns.agg1(int);
 
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_user1 REVOKE USAGE ON TYPES FROM public;
 
@@ -993,12 +1006,15 @@ CREATE TABLE testns.t2 (f1 int);
 SELECT has_table_privilege('regress_user1', 'testns.t2', 'SELECT'); -- false
 
 CREATE FUNCTION testns.testfunc(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
+CREATE AGGREGATE testns.testagg(int) (sfunc = int4pl, stype = int4);
 
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- true by default
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- true by default
 
 REVOKE ALL ON ALL FUNCTIONS IN SCHEMA testns FROM PUBLIC;
 
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- false
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- false
 
 \set VERBOSITY terse \\ -- suppress cascade details
 DROP SCHEMA testns CASCADE;
@@ -1062,6 +1078,7 @@ CREATE SCHEMA testns;
 
 drop sequence x_seq;
 
+DROP AGGREGATE testagg1(int);
 DROP FUNCTION testfunc2(int);
 DROP FUNCTION testfunc4(boolean);
 
-- 
2.15.0

#22Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Tom Lane (#13)
Re: [HACKERS] SQL procedures

On 11/8/17 09:54, Tom Lane wrote:

Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:

On 10/31/17 14:23, Tom Lane wrote:

Why not use VOIDOID for the prorettype value?

We need a way to distinguish functions that are callable by SELECT and
procedures that are callable by CALL.

Do procedures of this ilk belong in pg_proc at all? It seems like a large
fraction of the attributes tracked in pg_proc are senseless for this
purpose. A new catalog might be a better approach.

The common functionality between functions and procedures is like 98%
[citation needed], so they definitely belong there, even more so than
aggregates, for example.

In any case, I buy none of your arguments that 0 is a better choice than a
new pseudotype.

Well, I haven't heard any reasons for doing it differently, so I can't
judge the relative merits of either approach. Ultimately, it would be a
minor detail as far as the code is concerned, I think.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#23Tom Lane
tgl@sss.pgh.pa.us
In reply to: Peter Eisentraut (#22)
Re: [HACKERS] SQL procedures

Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:

On 11/8/17 09:54, Tom Lane wrote:

Do procedures of this ilk belong in pg_proc at all? It seems like a large
fraction of the attributes tracked in pg_proc are senseless for this
purpose. A new catalog might be a better approach.

The common functionality between functions and procedures is like 98%
[citation needed], so they definitely belong there, even more so than
aggregates, for example.

No, I don't think so. The core reason why not is that in

SELECT foo(...) FROM ...

foo() might be either a plain function or an aggregate, so it's important
that functions and aggregates share the same namespace. *That* is why
they are in the same catalog. On the other hand, since the above syntax
is not usable to call a SQL procedure, putting SQL procedures into pg_proc
just creates namespacing conflicts. Do we really want the existence of
a function foo(int) to mean that you can't create a SQL procedure named
foo and taking one int argument?

regards, tom lane

#24Pavel Stehule
pavel.stehule@gmail.com
In reply to: Tom Lane (#23)
Re: [HACKERS] SQL procedures

2017-11-14 17:14 GMT+01:00 Tom Lane <tgl@sss.pgh.pa.us>:

Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:

On 11/8/17 09:54, Tom Lane wrote:

Do procedures of this ilk belong in pg_proc at all? It seems like a

large

fraction of the attributes tracked in pg_proc are senseless for this
purpose. A new catalog might be a better approach.

The common functionality between functions and procedures is like 98%
[citation needed], so they definitely belong there, even more so than
aggregates, for example.

No, I don't think so. The core reason why not is that in

SELECT foo(...) FROM ...

foo() might be either a plain function or an aggregate, so it's important
that functions and aggregates share the same namespace. *That* is why
they are in the same catalog. On the other hand, since the above syntax
is not usable to call a SQL procedure, putting SQL procedures into pg_proc
just creates namespacing conflicts. Do we really want the existence of
a function foo(int) to mean that you can't create a SQL procedure named
foo and taking one int argument?

It is good point.

I agree so catalogue should be separate. Because procedures should not be
used in query, then lot of attributes has not sense there. Maybe in future,
we would to implement new features for procedures and it can be a problem
when we share catalogue with functions.

Show quoted text

regards, tom lane

#25Daniel Verite
daniel@manitou-mail.org
In reply to: Tom Lane (#23)
Re: [HACKERS] SQL procedures

Tom Lane wrote:

Do we really want the existence of a function foo(int) to mean
that you can't create a SQL procedure named
foo and taking one int argument?

Isn't it pretty much implied by the
ALTER | DROP ROUTINE foo(...)
commands where foo(...) may be either a procedure
or a function? It doesn't look like it could be both.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#26Simon Riggs
simon@2ndquadrant.com
In reply to: Daniel Verite (#25)
Re: [HACKERS] SQL procedures

On 14 November 2017 at 12:56, Daniel Verite <daniel@manitou-mail.org> wrote:

Tom Lane wrote:

Do we really want the existence of a function foo(int) to mean
that you can't create a SQL procedure named
foo and taking one int argument?

Isn't it pretty much implied by the
ALTER | DROP ROUTINE foo(...)
commands where foo(...) may be either a procedure
or a function? It doesn't look like it could be both.

It doesn't seem particularly troublesome to create another catalog
table, if needed, so that shouldn't drive our thinking.

It would seem to be implied by the SQLStandard that Functions and
Procedures occupy the same namespace, since they are both Routines.

I can't see any benefit from having foo() function AND foo() procedure
at same time. It would certainly confuse most people that come from
programming languages without that distinction, but maybe someone
knows some Oracle-foo that I don't?

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

#27Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Tom Lane (#23)
Re: [HACKERS] SQL procedures

On 11/14/17 11:14, Tom Lane wrote:

The common functionality between functions and procedures is like 98%
[citation needed], so they definitely belong there, even more so than
aggregates, for example.

No, I don't think so. The core reason why not is that in

SELECT foo(...) FROM ...

foo() might be either a plain function or an aggregate, so it's important
that functions and aggregates share the same namespace. *That* is why
they are in the same catalog. On the other hand, since the above syntax
is not usable to call a SQL procedure, putting SQL procedures into pg_proc
just creates namespacing conflicts. Do we really want the existence of
a function foo(int) to mean that you can't create a SQL procedure named
foo and taking one int argument?

Yes, that is defined that way by the SQL standard.

The point about the overlap refers more to the internals. The entire
parsing, look-up, type-resolution, DDL handling, and other things are
the same.

So for both of these reasons I think it's appropriate to use the same
catalog.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#28Tom Lane
tgl@sss.pgh.pa.us
In reply to: Peter Eisentraut (#27)
Re: [HACKERS] SQL procedures

Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:

On 11/14/17 11:14, Tom Lane wrote:

... Do we really want the existence of
a function foo(int) to mean that you can't create a SQL procedure named
foo and taking one int argument?

Yes, that is defined that way by the SQL standard.

Meh. OK, then it has to be one catalog.

regards, tom lane

#29Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Simon Riggs (#26)
Re: [HACKERS] SQL procedures

Simon Riggs wrote:

It would seem to be implied by the SQLStandard that Functions and
Procedures occupy the same namespace, since they are both Routines.

I can't see any benefit from having foo() function AND foo() procedure
at same time. It would certainly confuse most people that come from
programming languages without that distinction, but maybe someone
knows some Oracle-foo that I don't?

The question already has been decided on the (better) basis of
the SQL standard, but for what it is worth:

Oracle has functions and procedures in the same name space.

Yours,
Laurenz Albe

#30Andrew Dunstan
andrew.dunstan@2ndquadrant.com
In reply to: Peter Eisentraut (#21)
Re: [HACKERS] SQL procedures

On 11/14/2017 10:54 AM, Peter Eisentraut wrote:

Here is an updated patch. It's updated for the recent documentation
format changes. I added some more documentation as suggested by reviewers.

I also added more tests about how the various privilege commands (GRANT,
GRANT on ALL, default privileges) would work with the new object type.
I had not looked at that in much detail with the previous version of the
patch, but it seems to work the way I would have wanted without any code
changes, so it's all documentation additions and new tests.

As part of this I have also developed additional tests for how the same
privilege commands apply to aggregates, which didn't appear to be
covered yet, and I was worried that I might have broken it, which it
seems I did not. This is included in the big patch, but I have also
included it here as a separate patch, as it could be committed
independently as additional tests for existing functionality.

It this point, this patch has no more open TODOs or
need-to-think-about-this-later's that I'm aware of.

I've been through this fairly closely, and I think it's pretty much
committable. The only big item that stands out for me is the issue of
OUT parameters.

While returning multiple result sets will be a useful feature, it's also
very common in my experience for stored procedures to have scalar out
params as well. I'm not sure how we should go about providing for it,
but I think we need to be sure we're not closing any doors.

Here, for example, is how the MySQL stored procedure feature works with
JDBC:
<https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-usagenotes-statements-callable.html&gt;
I think it will be OK if we use cursors to return multiple result sets,
along the lines of Peter's next patch, but we shouldn't regard that as
the end of the story. Even if we don't provide for it in this go round
we should aim at eventually providing for stored procedure OUT params.

Apart from that here are a few fairly minor nitpicks:

Default Privileges docs: although FUNCTIONS and ROUTINES are equivalent
here, we should probably advise using ROUTINES, as FUNCTIONS could
easily be take to mean "functions but not procedures".

CREATE/ALTER PROCEDURE: It seems more than a little odd to allow
attributes that are irrelevant to procedures in these statements. The
docco states "for compatibility with ALTER FUNCTION" but why do we want
such compatibility if it's meaningless? If we can manage it without too
much violence I'd prefer to see an exception raised if these are used.

In create_function.sgml, we have:

        If a schema name is included, then the function is created in the
        specified schema.  Otherwise it is created in the current schema.
    -   The name of the new function must not match any existing function
    +   The name of the new function must not match any existing
    function or procedure
        with the same input argument types in the same schema.  However,
        functions of different argument types can share a name (this is
        called <firstterm>overloading</firstterm>).

The last sentence should probably say "functions and procedures of
different argument types" There's a similar issue in create_procedure.sqml.

In grant.sgml, there is:

    +       The <literal>FUNCTION</literal> syntax also works for aggregate
    +       functions.  Or use <literal>ROUTINE</literal> to refer to a
    function,
    +       aggregate function, or procedure regardless of what it is.

I would replace "Or" by "Alternatively,". I think it reads better that way.

In functions.c, there is:

                /* Should only get here for VOID functions */
    -           Assert(fcache->rettype == VOIDOID);
    +           Assert(fcache->rettype == InvalidOid || fcache->rettype
    == VOIDOID);
                fcinfo->isnull = true;
                result = (Datum) 0;

The comment should also refer to procedures.

It took me a minute to work out what is going on with the new code in
aclchk.c:objectsInSchemaToOids(). It probably merits a comment or two.

We should document where returned values in PL procedures are ignored
(plperl, pltcl) and where they are not (plpython, plpgsql). Maybe we
should think about possibly being more consistent here, too.

The context line here looks odd:

CREATE PROCEDURE test_proc2()
LANGUAGE plpythonu
AS $$
return 5
$$;
CALL test_proc2();
ERROR:  PL/Python procedure did not return None
CONTEXT:  PL/Python function "test_proc2"

Perhaps we need to change plpython_error_callback() so that "function"
isn't hardcoded.

cheers

andrew

--
Andrew Dunstan https://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#31Andrew Dunstan
andrew.dunstan@2ndquadrant.com
In reply to: Andrew Dunstan (#30)
Re: [HACKERS] SQL procedures

On 11/20/2017 04:25 PM, I wrote:

I've been through this fairly closely, and I think it's pretty much
committable. The only big item that stands out for me is the issue of
OUT parameters.

While returning multiple result sets will be a useful feature, it's also
very common in my experience for stored procedures to have scalar out
params as well. I'm not sure how we should go about providing for it,
but I think we need to be sure we're not closing any doors.

Here, for example, is how the MySQL stored procedure feature works with
JDBC:
<https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-usagenotes-statements-callable.html&gt;
I think it will be OK if we use cursors to return multiple result sets,
along the lines of Peter's next patch, but we shouldn't regard that as
the end of the story. Even if we don't provide for it in this go round
we should aim at eventually providing for stored procedure OUT params.

Of course it's true that we could replace a scalar OUT parameter with a
one row resultset if we have return of multiple resultsets from SPs. But
it's different from the common use pattern and a darn sight more
cumbersome to use.

cheers

andrew

--
Andrew Dunstan https://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#32Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Andrew Dunstan (#30)
Re: [HACKERS] SQL procedures

On 11/20/17 16:25, Andrew Dunstan wrote:

I've been through this fairly closely, and I think it's pretty much
committable. The only big item that stands out for me is the issue of
OUT parameters.

I figured that that's something that would come up. I had intentionally
prohibited OUT parameters for now so that we can come up with something
for them without having to break any backward compatibility.

From reading some of the references so far, I think it could be
sufficient to return a one-row result set and have the drivers adapt the
returned data into their interfaces. The only thing that would be a bit
weird about that is that a CALL command with OUT parameters would return
a result set and a CALL command without any OUT parameters would return
CommandComplete instead. Maybe that's OK.

Default Privileges docs: although FUNCTIONS and ROUTINES are equivalent
here, we should probably advise using ROUTINES, as FUNCTIONS could
easily be take to mean "functions but not procedures".

OK, I'll update the documentation a bit more.

CREATE/ALTER PROCEDURE: It seems more than a little odd to allow
attributes that are irrelevant to procedures in these statements. The
docco states "for compatibility with ALTER FUNCTION" but why do we want
such compatibility if it's meaningless? If we can manage it without too
much violence I'd prefer to see an exception raised if these are used.

We can easily be more restrictive here. I'm open to suggestions. There
is a difference between options that don't make sense for procedures
(e.g., RETURNS NULL ON NULL INPUT), which are prohibited, and those that
might make sense sometime, but are currently not used. But maybe that's
too confusing and we should just prohibit options that are not currently
used.

In create_function.sgml, we have:

    If a schema name is included, then the function is created in the
    specified schema.  Otherwise it is created in the current schema.
-   The name of the new function must not match any existing function
+   The name of the new function must not match any existing
function or procedure
    with the same input argument types in the same schema.  However,
    functions of different argument types can share a name (this is
    called <firstterm>overloading</firstterm>).

The last sentence should probably say "functions and procedures of
different argument types" There's a similar issue in create_procedure.sqml.

fixing that

In grant.sgml, there is:

+       The <literal>FUNCTION</literal> syntax also works for aggregate
+       functions.  Or use <literal>ROUTINE</literal> to refer to a
function,
+       aggregate function, or procedure regardless of what it is.

I would replace "Or" by "Alternatively,". I think it reads better that way.

fixing that

In functions.c, there is:

            /* Should only get here for VOID functions */
-           Assert(fcache->rettype == VOIDOID);
+           Assert(fcache->rettype == InvalidOid || fcache->rettype
== VOIDOID);
            fcinfo->isnull = true;
            result = (Datum) 0;

The comment should also refer to procedures.

fixing that

It took me a minute to work out what is going on with the new code in
aclchk.c:objectsInSchemaToOids(). It probably merits a comment or two.

improving that

We should document where returned values in PL procedures are ignored
(plperl, pltcl) and where they are not (plpython, plpgsql). Maybe we
should think about possibly being more consistent here, too.

Yeah, suggestions? I think it makes sense in PL/pgSQL and PL/Python to
disallow return values that would end up being ignored, because the only
way a return value could arise is if user code explicit calls
RETURN/return. However, in Perl, the return value is the result of the
last statement, so prohibiting a return value would be very
inconvenient. (Don't know about Tcl.) So maybe the current behavior
makes sense. Documentation is surely needed.

The context line here looks odd:

CREATE PROCEDURE test_proc2()
LANGUAGE plpythonu
AS $$
return 5
$$;
CALL test_proc2();
ERROR:  PL/Python procedure did not return None
CONTEXT:  PL/Python function "test_proc2"

Perhaps we need to change plpython_error_callback() so that "function"
isn't hardcoded.

fixing that

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#33Tom Lane
tgl@sss.pgh.pa.us
In reply to: Peter Eisentraut (#32)
Re: [HACKERS] SQL procedures

Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:

On 11/20/17 16:25, Andrew Dunstan wrote:

We should document where returned values in PL procedures are ignored
(plperl, pltcl) and where they are not (plpython, plpgsql). Maybe we
should think about possibly being more consistent here, too.

Yeah, suggestions? I think it makes sense in PL/pgSQL and PL/Python to
disallow return values that would end up being ignored, because the only
way a return value could arise is if user code explicit calls
RETURN/return. However, in Perl, the return value is the result of the
last statement, so prohibiting a return value would be very
inconvenient. (Don't know about Tcl.) So maybe the current behavior
makes sense. Documentation is surely needed.

Tcl has the same approach as Perl: the return value of a proc is the
same as the value of its last command. There's no such thing as a
proc that doesn't return a value.

regards, tom lane

#34Pavel Stehule
pavel.stehule@gmail.com
In reply to: Peter Eisentraut (#32)
Re: [HACKERS] SQL procedures

2017-11-22 19:01 GMT+01:00 Peter Eisentraut <
peter.eisentraut@2ndquadrant.com>:

On 11/20/17 16:25, Andrew Dunstan wrote:

I've been through this fairly closely, and I think it's pretty much
committable. The only big item that stands out for me is the issue of
OUT parameters.

I figured that that's something that would come up. I had intentionally
prohibited OUT parameters for now so that we can come up with something
for them without having to break any backward compatibility.

From reading some of the references so far, I think it could be
sufficient to return a one-row result set and have the drivers adapt the
returned data into their interfaces. The only thing that would be a bit
weird about that is that a CALL command with OUT parameters would return
a result set and a CALL command without any OUT parameters would return
CommandComplete instead. Maybe that's OK.

T-SQL procedures returns data or OUT variables.

I remember, it was very frustrating

Maybe first result can be reserved for OUT variables, others for multi
result set

Regards

Pavel

Show quoted text

Default Privileges docs: although FUNCTIONS and ROUTINES are equivalent
here, we should probably advise using ROUTINES, as FUNCTIONS could
easily be take to mean "functions but not procedures".

OK, I'll update the documentation a bit more.

CREATE/ALTER PROCEDURE: It seems more than a little odd to allow
attributes that are irrelevant to procedures in these statements. The
docco states "for compatibility with ALTER FUNCTION" but why do we want
such compatibility if it's meaningless? If we can manage it without too
much violence I'd prefer to see an exception raised if these are used.

We can easily be more restrictive here. I'm open to suggestions. There
is a difference between options that don't make sense for procedures
(e.g., RETURNS NULL ON NULL INPUT), which are prohibited, and those that
might make sense sometime, but are currently not used. But maybe that's
too confusing and we should just prohibit options that are not currently
used.

In create_function.sgml, we have:

If a schema name is included, then the function is created in the
specified schema.  Otherwise it is created in the current schema.
-   The name of the new function must not match any existing function
+   The name of the new function must not match any existing
function or procedure
with the same input argument types in the same schema.  However,
functions of different argument types can share a name (this is
called <firstterm>overloading</firstterm>).

The last sentence should probably say "functions and procedures of
different argument types" There's a similar issue in

create_procedure.sqml.

fixing that

In grant.sgml, there is:

+ The <literal>FUNCTION</literal> syntax also works for

aggregate

+       functions.  Or use <literal>ROUTINE</literal> to refer to a
function,
+       aggregate function, or procedure regardless of what it is.

I would replace "Or" by "Alternatively,". I think it reads better that

way.

fixing that

In functions.c, there is:

/* Should only get here for VOID functions */
-           Assert(fcache->rettype == VOIDOID);
+           Assert(fcache->rettype == InvalidOid || fcache->rettype
== VOIDOID);
fcinfo->isnull = true;
result = (Datum) 0;

The comment should also refer to procedures.

fixing that

It took me a minute to work out what is going on with the new code in
aclchk.c:objectsInSchemaToOids(). It probably merits a comment or two.

improving that

We should document where returned values in PL procedures are ignored
(plperl, pltcl) and where they are not (plpython, plpgsql). Maybe we
should think about possibly being more consistent here, too.

Yeah, suggestions? I think it makes sense in PL/pgSQL and PL/Python to
disallow return values that would end up being ignored, because the only
way a return value could arise is if user code explicit calls
RETURN/return. However, in Perl, the return value is the result of the
last statement, so prohibiting a return value would be very
inconvenient. (Don't know about Tcl.) So maybe the current behavior
makes sense. Documentation is surely needed.

The context line here looks odd:

CREATE PROCEDURE test_proc2()
LANGUAGE plpythonu
AS $$
return 5
$$;
CALL test_proc2();
ERROR: PL/Python procedure did not return None
CONTEXT: PL/Python function "test_proc2"

Perhaps we need to change plpython_error_callback() so that "function"
isn't hardcoded.

fixing that

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#35Andrew Dunstan
andrew.dunstan@2ndquadrant.com
In reply to: Tom Lane (#33)
Re: [HACKERS] SQL procedures

On 11/22/2017 01:10 PM, Tom Lane wrote:

Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:

On 11/20/17 16:25, Andrew Dunstan wrote:

We should document where returned values in PL procedures are ignored
(plperl, pltcl) and where they are not (plpython, plpgsql). Maybe we
should think about possibly being more consistent here, too.

Yeah, suggestions? I think it makes sense in PL/pgSQL and PL/Python to
disallow return values that would end up being ignored, because the only
way a return value could arise is if user code explicit calls
RETURN/return. However, in Perl, the return value is the result of the
last statement, so prohibiting a return value would be very
inconvenient. (Don't know about Tcl.) So maybe the current behavior
makes sense. Documentation is surely needed.

Tcl has the same approach as Perl: the return value of a proc is the
same as the value of its last command. There's no such thing as a
proc that doesn't return a value.

Right. We probably just need to document the behaviour here. I'd be
satisfied with that.

cheers

andrew

--
Andrew Dunstan https://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#36Corey Huinker
corey.huinker@gmail.com
In reply to: Pavel Stehule (#34)
Re: [HACKERS] SQL procedures

T-SQL procedures returns data or OUT variables.

I remember, it was very frustrating

Maybe first result can be reserved for OUT variables, others for multi
result set

It's been many years, but if I recall correctly, T-SQL returns a series of
result sets, with no description of the result sets to be returned, each of
which must be consumed fully before the client can move onto the next
result set. Then and only then can the output parameters be read. Which was
very frustrating because the OUT parameters seemed like a good place to put
values for things like "result set 1 has 205 rows" and "X was false so we
omitted one result set entirely" so you couldn't, y'know easily omit entire
result sets.

#37Andrew Dunstan
andrew.dunstan@2ndquadrant.com
In reply to: Corey Huinker (#36)
Re: [HACKERS] SQL procedures

On 11/22/2017 02:39 PM, Corey Huinker wrote:

T-SQL procedures returns data or OUT variables.

I remember, it was very frustrating

Maybe first result can be reserved for OUT variables, others for
multi result set

It's been many years, but if I recall correctly, T-SQL returns a
series of result sets, with no description of the result sets to be
returned, each of which must be consumed fully before the client can
move onto the next result set. Then and only then can the output
parameters be read. Which was very frustrating because the OUT
parameters seemed like a good place to put values for things like
"result set 1 has 205 rows" and "X was false so we omitted one result
set entirely" so you couldn't, y'know easily omit entire result sets. 

Exactly. If we want to handle OUT params this way they really need to be
the first resultset for just this reason. That could possibly be done by
the glue code reserving a spot in the resultset list and filling it in
at the end of the procedure.

cheers

andrew

--
Andrew Dunstan https://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#38Craig Ringer
craig@2ndquadrant.com
In reply to: Andrew Dunstan (#37)
Re: [HACKERS] SQL procedures

On 23 November 2017 at 03:47, Andrew Dunstan <andrew.dunstan@2ndquadrant.com

wrote:

On 11/22/2017 02:39 PM, Corey Huinker wrote:

T-SQL procedures returns data or OUT variables.

I remember, it was very frustrating

Maybe first result can be reserved for OUT variables, others for
multi result set

It's been many years, but if I recall correctly, T-SQL returns a
series of result sets, with no description of the result sets to be
returned, each of which must be consumed fully before the client can
move onto the next result set. Then and only then can the output
parameters be read. Which was very frustrating because the OUT
parameters seemed like a good place to put values for things like
"result set 1 has 205 rows" and "X was false so we omitted one result
set entirely" so you couldn't, y'know easily omit entire result sets.

Exactly. If we want to handle OUT params this way they really need to be
the first resultset for just this reason. That could possibly be done by
the glue code reserving a spot in the resultset list and filling it in
at the end of the procedure.

I fail to understand how that can work though. Wouldn't we have to buffer
all the resultset contents on the server in tuplestores or similar, so we
can send the parameters and then the result sets?

Isn't that going to cause a whole different set of painful difficulties
instead?

What it comes down to is that if we want to see output params before result
sets, but the output params are only emitted when the proc returns, then
someone has to buffer. We get to choose if it's the client or the server.

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

#39Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Craig Ringer (#38)
Re: [HACKERS] SQL procedures

On 11/23/17 00:59, Craig Ringer wrote:

Exactly. If we want to handle OUT params this way they really need to be
the first resultset for just this reason. That could possibly be done by
the glue code reserving a spot in the resultset list and filling it in
at the end of the procedure.

I fail to understand how that can work though. Wouldn't we have to
buffer all the resultset contents on the server in tuplestores or
similar, so we can send the parameters and then the result sets?

The current PoC in the other thread puts the extra result sets in
cursors. That's essentially the buffer you are referring to. So it
seems possible, but there are some details to be worked out.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#40Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Peter Eisentraut (#32)
1 attachment(s)
Re: [HACKERS] SQL procedures

Here is a new patch that addresses the previous review comments.

If there are no new comments, I think this might be ready to go.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

v3-0001-SQL-procedures.patchtext/plain; charset=UTF-8; name=v3-0001-SQL-procedures.patch; x-mac-creator=0; x-mac-type=0Download
From 29d9779ce89757e48016586c4e8f491fe3b91373 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Tue, 28 Nov 2017 10:00:59 -0500
Subject: [PATCH v3] SQL procedures

CREATE/ALTER/DROP PROCEDURE, ALTER/DROP ROUTINE, CALL statement, utility
statement support, support in the built-in PLs, support in pg_dump and
psql
---
 doc/src/sgml/catalogs.sgml                     |   2 +-
 doc/src/sgml/ddl.sgml                          |   2 +-
 doc/src/sgml/ecpg.sgml                         |   4 +-
 doc/src/sgml/information_schema.sgml           |  18 +-
 doc/src/sgml/plperl.sgml                       |   4 +
 doc/src/sgml/plpgsql.sgml                      |  17 +-
 doc/src/sgml/plpython.sgml                     |   6 +-
 doc/src/sgml/pltcl.sgml                        |   3 +-
 doc/src/sgml/ref/allfiles.sgml                 |   6 +
 doc/src/sgml/ref/alter_default_privileges.sgml |  12 +-
 doc/src/sgml/ref/alter_extension.sgml          |  12 +-
 doc/src/sgml/ref/alter_function.sgml           |   2 +
 doc/src/sgml/ref/alter_procedure.sgml          | 281 ++++++++++++++++++++
 doc/src/sgml/ref/alter_routine.sgml            | 102 ++++++++
 doc/src/sgml/ref/call.sgml                     |  97 +++++++
 doc/src/sgml/ref/comment.sgml                  |  13 +-
 doc/src/sgml/ref/create_function.sgml          |  10 +-
 doc/src/sgml/ref/create_procedure.sgml         | 341 +++++++++++++++++++++++++
 doc/src/sgml/ref/drop_function.sgml            |   2 +
 doc/src/sgml/ref/drop_procedure.sgml           | 162 ++++++++++++
 doc/src/sgml/ref/drop_routine.sgml             |  94 +++++++
 doc/src/sgml/ref/grant.sgml                    |  25 +-
 doc/src/sgml/ref/revoke.sgml                   |   4 +-
 doc/src/sgml/ref/security_label.sgml           |  12 +-
 doc/src/sgml/reference.sgml                    |   6 +
 doc/src/sgml/xfunc.sgml                        |  33 +++
 src/backend/catalog/aclchk.c                   |  68 ++++-
 src/backend/catalog/information_schema.sql     |  25 +-
 src/backend/catalog/objectaddress.c            |  19 +-
 src/backend/catalog/pg_proc.c                  |   3 +-
 src/backend/commands/aggregatecmds.c           |   2 +-
 src/backend/commands/alter.c                   |   6 +
 src/backend/commands/dropcmds.c                |  38 ++-
 src/backend/commands/event_trigger.c           |  14 +
 src/backend/commands/functioncmds.c            | 164 +++++++++++-
 src/backend/commands/opclasscmds.c             |   4 +-
 src/backend/executor/functions.c               |  15 +-
 src/backend/nodes/copyfuncs.c                  |  15 ++
 src/backend/nodes/equalfuncs.c                 |  13 +
 src/backend/optimizer/util/clauses.c           |   1 +
 src/backend/parser/gram.y                      | 255 +++++++++++++++++-
 src/backend/parser/parse_agg.c                 |  11 +
 src/backend/parser/parse_expr.c                |   8 +
 src/backend/parser/parse_func.c                | 201 +++++++++------
 src/backend/tcop/utility.c                     |  44 +++-
 src/backend/utils/adt/ruleutils.c              |   6 +
 src/backend/utils/cache/lsyscache.c            |  19 ++
 src/bin/pg_dump/dumputils.c                    |   5 +-
 src/bin/pg_dump/pg_backup_archiver.c           |   7 +-
 src/bin/pg_dump/pg_dump.c                      |  32 ++-
 src/bin/pg_dump/t/002_pg_dump.pl               |  38 +++
 src/bin/psql/describe.c                        |   8 +-
 src/bin/psql/tab-complete.c                    |  77 +++++-
 src/include/commands/defrem.h                  |   3 +-
 src/include/nodes/nodes.h                      |   1 +
 src/include/nodes/parsenodes.h                 |  17 ++
 src/include/parser/kwlist.h                    |   4 +
 src/include/parser/parse_func.h                |   8 +-
 src/include/parser/parse_node.h                |   3 +-
 src/include/utils/lsyscache.h                  |   1 +
 src/interfaces/ecpg/preproc/ecpg.tokens        |   2 +-
 src/interfaces/ecpg/preproc/ecpg.trailer       |   5 +-
 src/interfaces/ecpg/preproc/ecpg_keywords.c    |   1 -
 src/pl/plperl/GNUmakefile                      |   2 +-
 src/pl/plperl/expected/plperl_call.out         |  29 +++
 src/pl/plperl/plperl.c                         |   8 +-
 src/pl/plperl/sql/plperl_call.sql              |  36 +++
 src/pl/plpgsql/src/pl_comp.c                   |  88 ++++---
 src/pl/plpgsql/src/pl_exec.c                   |   8 +-
 src/pl/plpython/Makefile                       |   1 +
 src/pl/plpython/expected/plpython_call.out     |  35 +++
 src/pl/plpython/plpy_exec.c                    |  14 +-
 src/pl/plpython/plpy_main.c                    |  10 +-
 src/pl/plpython/plpy_procedure.c               |   5 +-
 src/pl/plpython/plpy_procedure.h               |   3 +-
 src/pl/plpython/sql/plpython_call.sql          |  41 +++
 src/pl/tcl/Makefile                            |   2 +-
 src/pl/tcl/expected/pltcl_call.out             |  29 +++
 src/pl/tcl/pltcl.c                             |  13 +-
 src/pl/tcl/sql/pltcl_call.sql                  |  36 +++
 src/test/regress/expected/create_procedure.out |  92 +++++++
 src/test/regress/expected/object_address.out   |  15 +-
 src/test/regress/expected/plpgsql.out          |  41 +++
 src/test/regress/expected/polymorphism.out     |  16 +-
 src/test/regress/expected/privileges.out       | 128 +++++++++-
 src/test/regress/parallel_schedule             |   2 +-
 src/test/regress/serial_schedule               |   1 +
 src/test/regress/sql/create_procedure.sql      |  79 ++++++
 src/test/regress/sql/object_address.sql        |   4 +-
 src/test/regress/sql/plpgsql.sql               |  49 ++++
 src/test/regress/sql/privileges.sql            |  55 +++-
 91 files changed, 2951 insertions(+), 304 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_procedure.sgml
 create mode 100644 doc/src/sgml/ref/alter_routine.sgml
 create mode 100644 doc/src/sgml/ref/call.sgml
 create mode 100644 doc/src/sgml/ref/create_procedure.sgml
 create mode 100644 doc/src/sgml/ref/drop_procedure.sgml
 create mode 100644 doc/src/sgml/ref/drop_routine.sgml
 create mode 100644 src/pl/plperl/expected/plperl_call.out
 create mode 100644 src/pl/plperl/sql/plperl_call.sql
 create mode 100644 src/pl/plpython/expected/plpython_call.out
 create mode 100644 src/pl/plpython/sql/plpython_call.sql
 create mode 100644 src/pl/tcl/expected/pltcl_call.out
 create mode 100644 src/pl/tcl/sql/pltcl_call.sql
 create mode 100644 src/test/regress/expected/create_procedure.out
 create mode 100644 src/test/regress/sql/create_procedure.sql

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index da881a7737..3f02202caf 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5241,7 +5241,7 @@ <title><structname>pg_proc</structname> Columns</title>
       <entry><structfield>prorettype</structfield></entry>
       <entry><type>oid</type></entry>
       <entry><literal><link linkend="catalog-pg-type"><structname>pg_type</structname></link>.oid</literal></entry>
-      <entry>Data type of the return value</entry>
+      <entry>Data type of the return value, or null for a procedure</entry>
      </row>
 
      <row>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index e6f50ec819..9f583266de 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3947,7 +3947,7 @@ <title>Other Database Objects</title>
 
    <listitem>
     <para>
-     Functions and operators
+     Functions, procedures, and operators
     </para>
    </listitem>
 
diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml
index d1872c1a5c..5a8d1f1b95 100644
--- a/doc/src/sgml/ecpg.sgml
+++ b/doc/src/sgml/ecpg.sgml
@@ -4778,7 +4778,9 @@ <title>Setting Callbacks</title>
       <term><literal>DO <replaceable>name</replaceable> (<replaceable>args</replaceable>)</literal></term>
       <listitem>
        <para>
-        Call the specified C functions with the specified arguments.
+        Call the specified C functions with the specified arguments.  (This
+        use is different from the meaning of <literal>CALL</literal>
+        and <literal>DO</literal> in the normal PostgreSQL grammar.)
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 99b0ea8519..0faa72f1d3 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -3972,8 +3972,8 @@ <title><literal>routine_privileges</literal> Columns</title>
   <title><literal>routines</literal></title>
 
   <para>
-   The view <literal>routines</literal> contains all functions in the
-   current database.  Only those functions are shown that the current
+   The view <literal>routines</literal> contains all functions and procedures in the
+   current database.  Only those functions and procedures are shown that the current
    user has access to (by way of being the owner or having some
    privilege).
   </para>
@@ -4037,8 +4037,8 @@ <title><literal>routines</literal> Columns</title>
       <entry><literal>routine_type</literal></entry>
       <entry><type>character_data</type></entry>
       <entry>
-       Always <literal>FUNCTION</literal> (In the future there might
-       be other types of routines.)
+       <literal>FUNCTION</literal> for a
+       function, <literal>PROCEDURE</literal> for a procedure
       </entry>
      </row>
 
@@ -4087,7 +4087,7 @@ <title><literal>routines</literal> Columns</title>
        the view <literal>element_types</literal>), else
        <literal>USER-DEFINED</literal> (in that case, the type is
        identified in <literal>type_udt_name</literal> and associated
-       columns).
+       columns).  Null for a procedure.
       </entry>
      </row>
 
@@ -4180,7 +4180,7 @@ <title><literal>routines</literal> Columns</title>
       <entry><type>sql_identifier</type></entry>
       <entry>
        Name of the database that the return data type of the function
-       is defined in (always the current database)
+       is defined in (always the current database).  Null for a procedure.
       </entry>
      </row>
 
@@ -4189,7 +4189,7 @@ <title><literal>routines</literal> Columns</title>
       <entry><type>sql_identifier</type></entry>
       <entry>
        Name of the schema that the return data type of the function is
-       defined in
+       defined in.  Null for a procedure.
       </entry>
      </row>
 
@@ -4197,7 +4197,7 @@ <title><literal>routines</literal> Columns</title>
       <entry><literal>type_udt_name</literal></entry>
       <entry><type>sql_identifier</type></entry>
       <entry>
-       Name of the return data type of the function
+       Name of the return data type of the function.  Null for a procedure.
       </entry>
      </row>
 
@@ -4314,7 +4314,7 @@ <title><literal>routines</literal> Columns</title>
       <entry>
        If the function automatically returns null if any of its
        arguments are null, then <literal>YES</literal>, else
-       <literal>NO</literal>.
+       <literal>NO</literal>.  Null for a procedure.
       </entry>
      </row>
 
diff --git a/doc/src/sgml/plperl.sgml b/doc/src/sgml/plperl.sgml
index 33e39d85e4..100162dead 100644
--- a/doc/src/sgml/plperl.sgml
+++ b/doc/src/sgml/plperl.sgml
@@ -67,6 +67,10 @@ <title>PL/Perl Functions and Arguments</title>
    as discussed below.
   </para>
 
+  <para>
+   In a PL/Perl procedure, any return value from the Perl code is ignored.
+  </para>
+
   <para>
    PL/Perl also supports anonymous code blocks called with the
    <xref linkend="sql-do"/> statement:
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 6d14b34448..7d23ed437e 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -156,7 +156,8 @@ <title>Supported Argument and Result Data Types</title>
 
     <para>
      Finally, a <application>PL/pgSQL</application> function can be declared to return
-     <type>void</type> if it has no useful return value.
+     <type>void</type> if it has no useful return value.  (Alternatively, it
+     could be written as a procedure in that case.)
     </para>
 
     <para>
@@ -1865,6 +1866,18 @@ <title><command>RETURN NEXT</command> and <command>RETURN QUERY</command></title
     </sect3>
    </sect2>
 
+   <sect2 id="plpgsql-statements-returning-procedure">
+    <title>Returning From a Procedure</title>
+
+    <para>
+     A procedure does not have a return value.  A procedure can therefore end
+     without a <command>RETURN</command> statement.  If
+     a <command>RETURN</command> statement is desired to exit the code early,
+     then <symbol>NULL</symbol> must be returned.  Returning any other value
+     will result in an error.
+    </para>
+   </sect2>
+
    <sect2 id="plpgsql-conditionals">
     <title>Conditionals</title>
 
@@ -5244,7 +5257,7 @@ <title>Porting a Function that Creates Another Function from <application>PL/SQL
     <para>
      Here is how this function would end up in <productname>PostgreSQL</productname>:
 <programlisting>
-CREATE OR REPLACE FUNCTION cs_update_referrer_type_proc() RETURNS void AS $func$
+CREATE OR REPLACE PROCEDURE cs_update_referrer_type_proc() AS $func$
 DECLARE
     referrer_keys CURSOR IS
         SELECT * FROM cs_referrer_keys
diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml
index ec5f671632..0dbeee1fa2 100644
--- a/doc/src/sgml/plpython.sgml
+++ b/doc/src/sgml/plpython.sgml
@@ -207,7 +207,11 @@ <title>PL/Python Functions</title>
    <literal>yield</literal> (in case of a result-set statement).  If
    you do not provide a return value, Python returns the default
    <symbol>None</symbol>. <application>PL/Python</application> translates
-   Python's <symbol>None</symbol> into the SQL null value.
+   Python's <symbol>None</symbol> into the SQL null value.  In a procedure,
+   the result from the Python code must be <symbol>None</symbol> (typically
+   achieved by ending the procedure without a <literal>return</literal>
+   statement or by using a <literal>return</literal> statement without
+   argument); otherwise, an error will be raised.
   </para>
 
   <para>
diff --git a/doc/src/sgml/pltcl.sgml b/doc/src/sgml/pltcl.sgml
index 0646a8ba0b..8018783b0a 100644
--- a/doc/src/sgml/pltcl.sgml
+++ b/doc/src/sgml/pltcl.sgml
@@ -97,7 +97,8 @@ <title>PL/Tcl Functions and Arguments</title>
      Tcl script as variables named <literal>1</literal>
      ... <literal><replaceable>n</replaceable></literal>.  The result is
      returned from the Tcl code in the usual way, with
-     a <literal>return</literal> statement.
+     a <literal>return</literal> statement.  In a procedure, the return value
+     from the Tcl code is ignored.
     </para>
 
     <para>
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 01acc2ef9d..22e6893211 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -26,8 +26,10 @@
 <!ENTITY alterOperatorClass SYSTEM "alter_opclass.sgml">
 <!ENTITY alterOperatorFamily SYSTEM "alter_opfamily.sgml">
 <!ENTITY alterPolicy        SYSTEM "alter_policy.sgml">
+<!ENTITY alterProcedure     SYSTEM "alter_procedure.sgml">
 <!ENTITY alterPublication   SYSTEM "alter_publication.sgml">
 <!ENTITY alterRole          SYSTEM "alter_role.sgml">
+<!ENTITY alterRoutine       SYSTEM "alter_routine.sgml">
 <!ENTITY alterRule          SYSTEM "alter_rule.sgml">
 <!ENTITY alterSchema        SYSTEM "alter_schema.sgml">
 <!ENTITY alterServer        SYSTEM "alter_server.sgml">
@@ -48,6 +50,7 @@
 <!ENTITY alterView          SYSTEM "alter_view.sgml">
 <!ENTITY analyze            SYSTEM "analyze.sgml">
 <!ENTITY begin              SYSTEM "begin.sgml">
+<!ENTITY call               SYSTEM "call.sgml">
 <!ENTITY checkpoint         SYSTEM "checkpoint.sgml">
 <!ENTITY close              SYSTEM "close.sgml">
 <!ENTITY cluster            SYSTEM "cluster.sgml">
@@ -75,6 +78,7 @@
 <!ENTITY createOperatorClass SYSTEM "create_opclass.sgml">
 <!ENTITY createOperatorFamily SYSTEM "create_opfamily.sgml">
 <!ENTITY createPolicy       SYSTEM "create_policy.sgml">
+<!ENTITY createProcedure    SYSTEM "create_procedure.sgml">
 <!ENTITY createPublication  SYSTEM "create_publication.sgml">
 <!ENTITY createRole         SYSTEM "create_role.sgml">
 <!ENTITY createRule         SYSTEM "create_rule.sgml">
@@ -122,8 +126,10 @@
 <!ENTITY dropOperatorFamily  SYSTEM "drop_opfamily.sgml">
 <!ENTITY dropOwned          SYSTEM "drop_owned.sgml">
 <!ENTITY dropPolicy         SYSTEM "drop_policy.sgml">
+<!ENTITY dropProcedure      SYSTEM "drop_procedure.sgml">
 <!ENTITY dropPublication    SYSTEM "drop_publication.sgml">
 <!ENTITY dropRole           SYSTEM "drop_role.sgml">
+<!ENTITY dropRoutine        SYSTEM "drop_routine.sgml">
 <!ENTITY dropRule           SYSTEM "drop_rule.sgml">
 <!ENTITY dropSchema         SYSTEM "drop_schema.sgml">
 <!ENTITY dropSequence       SYSTEM "drop_sequence.sgml">
diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml
index ab2c35b4dd..0c09f1db5c 100644
--- a/doc/src/sgml/ref/alter_default_privileges.sgml
+++ b/doc/src/sgml/ref/alter_default_privileges.sgml
@@ -39,7 +39,7 @@
     TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ]
 
 GRANT { EXECUTE | ALL [ PRIVILEGES ] }
-    ON FUNCTIONS
+    ON { FUNCTIONS | ROUTINES }
     TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ]
 
 GRANT { USAGE | ALL [ PRIVILEGES ] }
@@ -66,7 +66,7 @@
 
 REVOKE [ GRANT OPTION FOR ]
     { EXECUTE | ALL [ PRIVILEGES ] }
-    ON FUNCTIONS
+    ON { FUNCTIONS | ROUTINES }
     FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
     [ CASCADE | RESTRICT ]
 
@@ -93,7 +93,13 @@ <title>Description</title>
    affect privileges assigned to already-existing objects.)  Currently,
    only the privileges for schemas, tables (including views and foreign
    tables), sequences, functions, and types (including domains) can be
-   altered.
+   altered.  For this command, functions include aggregates and procedures.
+   The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are
+   equivalent in this command.  (<literal>ROUTINES</literal> is preferred
+   going forward as the standard term for functions and procedures taken
+   together.  In earlier PostgreSQL releases, only the
+   word <literal>FUNCTIONS</literal> was allowed.  It is not possible to set
+   default privileges for functions and procedures separately.)
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_extension.sgml b/doc/src/sgml/ref/alter_extension.sgml
index e54925507e..a2d405d6cd 100644
--- a/doc/src/sgml/ref/alter_extension.sgml
+++ b/doc/src/sgml/ref/alter_extension.sgml
@@ -45,6 +45,8 @@
   OPERATOR CLASS <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   OPERATOR FAMILY <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
+  PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
+  ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   SCHEMA <replaceable class="parameter">object_name</replaceable> |
   SEQUENCE <replaceable class="parameter">object_name</replaceable> |
   SERVER <replaceable class="parameter">object_name</replaceable> |
@@ -170,12 +172,14 @@ <title>Parameters</title>
      <term><replaceable class="parameter">aggregate_name</replaceable></term>
      <term><replaceable class="parameter">function_name</replaceable></term>
      <term><replaceable class="parameter">operator_name</replaceable></term>
+     <term><replaceable class="parameter">procedure_name</replaceable></term>
+     <term><replaceable class="parameter">routine_name</replaceable></term>
      <listitem>
       <para>
        The name of an object to be added to or removed from the extension.
        Names of tables,
        aggregates, domains, foreign tables, functions, operators,
-       operator classes, operator families, sequences, text search objects,
+       operator classes, operator families, procedures, routines, sequences, text search objects,
        types, and views can be schema-qualified.
       </para>
      </listitem>
@@ -204,7 +208,7 @@ <title>Parameters</title>
 
      <listitem>
       <para>
-       The mode of a function or aggregate
+       The mode of a function, procedure, or aggregate
        argument: <literal>IN</literal>, <literal>OUT</literal>,
        <literal>INOUT</literal>, or <literal>VARIADIC</literal>.
        If omitted, the default is <literal>IN</literal>.
@@ -222,7 +226,7 @@ <title>Parameters</title>
 
      <listitem>
       <para>
-       The name of a function or aggregate argument.
+       The name of a function, procedure, or aggregate argument.
        Note that <command>ALTER EXTENSION</command> does not actually pay
        any attention to argument names, since only the argument data
        types are needed to determine the function's identity.
@@ -235,7 +239,7 @@ <title>Parameters</title>
 
      <listitem>
       <para>
-       The data type of a function or aggregate argument.
+       The data type of a function, procedure, or aggregate argument.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/alter_function.sgml b/doc/src/sgml/ref/alter_function.sgml
index 196d2dde0c..d8747e0748 100644
--- a/doc/src/sgml/ref/alter_function.sgml
+++ b/doc/src/sgml/ref/alter_function.sgml
@@ -359,6 +359,8 @@ <title>See Also</title>
   <simplelist type="inline">
    <member><xref linkend="sql-createfunction"/></member>
    <member><xref linkend="sql-dropfunction"/></member>
+   <member><xref linkend="sql-alterprocedure"/></member>
+   <member><xref linkend="sql-alterroutine"/></member>
   </simplelist>
  </refsect1>
 </refentry>
diff --git a/doc/src/sgml/ref/alter_procedure.sgml b/doc/src/sgml/ref/alter_procedure.sgml
new file mode 100644
index 0000000000..dae80076d9
--- /dev/null
+++ b/doc/src/sgml/ref/alter_procedure.sgml
@@ -0,0 +1,281 @@
+<!--
+doc/src/sgml/ref/alter_procedure.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterprocedure">
+ <indexterm zone="sql-alterprocedure">
+  <primary>ALTER PROCEDURE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER PROCEDURE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER PROCEDURE</refname>
+  <refpurpose>change the definition of a procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    <replaceable class="parameter">action</replaceable> [ ... ] [ RESTRICT ]
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    RENAME TO <replaceable>new_name</replaceable>
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_USER | SESSION_USER }
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    SET SCHEMA <replaceable>new_schema</replaceable>
+ALTER PROCEDURE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    DEPENDS ON EXTENSION <replaceable>extension_name</replaceable>
+
+<phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
+
+    [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
+    SET <replaceable class="parameter">configuration_parameter</replaceable> { TO | = } { <replaceable class="parameter">value</replaceable> | DEFAULT }
+    SET <replaceable class="parameter">configuration_parameter</replaceable> FROM CURRENT
+    RESET <replaceable class="parameter">configuration_parameter</replaceable>
+    RESET ALL
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER PROCEDURE</command> changes the definition of a
+   procedure.
+  </para>
+
+  <para>
+   You must own the procedure to use <command>ALTER PROCEDURE</command>.
+   To change a procedure's schema, you must also have <literal>CREATE</literal>
+   privilege on the new schema.
+   To alter the owner, you must also be a direct or indirect member of the new
+   owning role, and that role must have <literal>CREATE</literal> privilege on
+   the procedure's schema.  (These restrictions enforce that altering the owner
+   doesn't do anything you couldn't do by dropping and recreating the procedure.
+   However, a superuser can alter ownership of any procedure anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing procedure.  If no
+      argument list is specified, the name must be unique in its schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argmode</replaceable></term>
+
+    <listitem>
+     <para>
+      The mode of an argument: <literal>IN</literal>  or <literal>VARIADIC</literal>.
+      If omitted, the default is <literal>IN</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argname</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of an argument.
+      Note that <command>ALTER PROCEDURE</command> does not actually pay
+      any attention to argument names, since only the argument data
+      types are needed to determine the procedure's identity.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argtype</replaceable></term>
+
+    <listitem>
+     <para>
+      The data type(s) of the procedure's arguments (optionally
+      schema-qualified), if any.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the procedure.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the procedure.  Note that if the procedure is
+      marked <literal>SECURITY DEFINER</literal>, it will subsequently
+      execute as the new owner.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the procedure.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">extension_name</replaceable></term>
+    <listitem>
+     <para>
+      The name of the extension that the procedure is to depend on.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal><optional> EXTERNAL </optional> SECURITY INVOKER</literal></term>
+    <term><literal><optional> EXTERNAL </optional> SECURITY DEFINER</literal></term>
+
+    <listitem>
+     <para>
+      Change whether the procedure is a security definer or not. The
+      key word <literal>EXTERNAL</literal> is ignored for SQL
+      conformance. See <xref linkend="sql-createprocedure"/> for more information about
+      this capability.
+     </para>
+    </listitem>
+   </varlistentry>
+
+     <varlistentry>
+      <term><replaceable>configuration_parameter</replaceable></term>
+      <term><replaceable>value</replaceable></term>
+      <listitem>
+       <para>
+        Add or change the assignment to be made to a configuration parameter
+        when the procedure is called.  If
+        <replaceable>value</replaceable> is <literal>DEFAULT</literal>
+        or, equivalently, <literal>RESET</literal> is used, the procedure-local
+        setting is removed, so that the procedure executes with the value
+        present in its environment.  Use <literal>RESET
+        ALL</literal> to clear all procedure-local settings.
+        <literal>SET FROM CURRENT</literal> saves the value of the parameter that
+        is current when <command>ALTER PROCEDURE</command> is executed as the value
+        to be applied when the procedure is entered.
+       </para>
+
+       <para>
+        See <xref linkend="sql-set"/> and
+        <xref linkend="runtime-config"/>
+        for more information about allowed parameter names and values.
+       </para>
+      </listitem>
+     </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+
+    <listitem>
+     <para>
+      Ignored for conformance with the SQL standard.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the procedure <literal>insert_data</literal> with two arguments
+   of type <type>integer</type> to <literal>insert_record</literal>:
+<programlisting>
+ALTER PROCEDURE insert_data(integer, integer) RENAME TO insert_record;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the procedure <literal>insert_data</literal> with
+   two arguments of type <type>integer</type> to <literal>joe</literal>:
+<programlisting>
+ALTER PROCEDURE insert_data(integer, integer) OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To change the schema of the procedure <literal>insert_data</literal> with
+   two arguments of type <type>integer</type>
+   to <literal>accounting</literal>:
+<programlisting>
+ALTER PROCEDURE insert_data(integer, integer) SET SCHEMA accounting;
+</programlisting>
+  </para>
+
+  <para>
+   To mark the procedure <literal>insert_data(integer, integer)</literal> as
+   being dependent on the extension <literal>myext</literal>:
+<programlisting>
+ALTER PROCEDURE insert_data(integer, integer) DEPENDS ON EXTENSION myext;
+</programlisting>
+  </para>
+
+  <para>
+   To adjust the search path that is automatically set for a procedure:
+<programlisting>
+ALTER PROCEDURE check_password(text) SET search_path = admin, pg_temp;
+</programlisting>
+  </para>
+
+  <para>
+   To disable automatic setting of <varname>search_path</varname> for a procedure:
+<programlisting>
+ALTER PROCEDURE check_password(text) RESET search_path;
+</programlisting>
+   The procedure will now execute with whatever search path is used by its
+   caller.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   This statement is partially compatible with the <command>ALTER
+   PROCEDURE</command> statement in the SQL standard. The standard allows more
+   properties of a procedure to be modified, but does not provide the
+   ability to rename a procedure, make a procedure a security definer,
+   attach configuration parameter values to a procedure,
+   or change the owner, schema, or volatility of a procedure. The standard also
+   requires the <literal>RESTRICT</literal> key word, which is optional in
+   <productname>PostgreSQL</productname>.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createprocedure"/></member>
+   <member><xref linkend="sql-dropprocedure"/></member>
+   <member><xref linkend="sql-alterfunction"/></member>
+   <member><xref linkend="sql-alterroutine"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_routine.sgml b/doc/src/sgml/ref/alter_routine.sgml
new file mode 100644
index 0000000000..d1699691e1
--- /dev/null
+++ b/doc/src/sgml/ref/alter_routine.sgml
@@ -0,0 +1,102 @@
+<!--
+doc/src/sgml/ref/alter_routine.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterroutine">
+ <indexterm zone="sql-alterroutine">
+  <primary>ALTER ROUTINE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER ROUTINE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER ROUTINE</refname>
+  <refpurpose>change the definition of a routine</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    <replaceable class="parameter">action</replaceable> [ ... ] [ RESTRICT ]
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    RENAME TO <replaceable>new_name</replaceable>
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_USER | SESSION_USER }
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    SET SCHEMA <replaceable>new_schema</replaceable>
+ALTER ROUTINE <replaceable>name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ]
+    DEPENDS ON EXTENSION <replaceable>extension_name</replaceable>
+
+<phrase>where <replaceable class="parameter">action</replaceable> is one of:</phrase>
+
+    IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF
+    [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
+    PARALLEL { UNSAFE | RESTRICTED | SAFE }
+    COST <replaceable class="parameter">execution_cost</replaceable>
+    ROWS <replaceable class="parameter">result_rows</replaceable>
+    SET <replaceable class="parameter">configuration_parameter</replaceable> { TO | = } { <replaceable class="parameter">value</replaceable> | DEFAULT }
+    SET <replaceable class="parameter">configuration_parameter</replaceable> FROM CURRENT
+    RESET <replaceable class="parameter">configuration_parameter</replaceable>
+    RESET ALL
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER ROUTINE</command> changes the definition of a routine, which
+   can be an aggregate function, a normal function, or a procedure.  See
+   under <xref linkend="sql-alteraggregate"/>, <xref linkend="sql-alterfunction"/>,
+   and <xref linkend="sql-alterprocedure"/> for the description of the
+   parameters, more examples, and further details.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the routine <literal>foo</literal> for type
+   <type>integer</type> to <literal>foobar</literal>:
+<programlisting>
+ALTER ROUTINE foo(integer) RENAME TO foobar;
+</programlisting>
+   This command will work independent of whether <literal>foo</literal> is an
+   aggregate, function, or procedure.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   This statement is partially compatible with the <command>ALTER
+   ROUTINE</command> statement in the SQL standard.  See
+   under <xref linkend="sql-alterfunction"/>
+   and <xref linkend="sql-alterprocedure"/> for more details.  Allowing
+   routine names to refer to aggregate functions is
+   a <productname>PostgreSQL</productname> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alteraggregate"/></member>
+   <member><xref linkend="sql-alterfunction"/></member>
+   <member><xref linkend="sql-alterprocedure"/></member>
+   <member><xref linkend="sql-droproutine"/></member>
+  </simplelist>
+
+  <para>
+   Note that there is no <literal>CREATE ROUTINE</literal> command.
+  </para>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/call.sgml b/doc/src/sgml/ref/call.sgml
new file mode 100644
index 0000000000..2741d8d15e
--- /dev/null
+++ b/doc/src/sgml/ref/call.sgml
@@ -0,0 +1,97 @@
+<!--
+doc/src/sgml/ref/call.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-call">
+ <indexterm zone="sql-call">
+  <primary>CALL</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CALL</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CALL</refname>
+  <refpurpose>invoke a procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CALL <replaceable class="parameter">name</replaceable> ( [ <replaceable class="parameter">argument</replaceable> ] [ , ...] )
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CALL</command> executes a procedure.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the procedure.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  <varlistentry>
+    <term><replaceable class="parameter">argument</replaceable></term>
+    <listitem>
+     <para>
+      An argument for the procedure call.
+      See <xref linkend="sql-syntax-calling-funcs"/> for the full details on
+      function and procedure call syntax, including use of named parameters.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   The user must have <literal>EXECUTE</literal> privilege on the procedure in
+   order to be allowed to invoke it.
+  </para>
+
+  <para>
+   To call a function (not a procedure), use <command>SELECT</command> instead.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+<programlisting>
+CALL do_db_maintenance();
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CALL</command> conforms to the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createprocedure"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 7d66c1a34c..965c5a40ad 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -46,8 +46,10 @@
   OPERATOR FAMILY <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   POLICY <replaceable class="parameter">policy_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
+  PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   PUBLICATION <replaceable class="parameter">object_name</replaceable> |
   ROLE <replaceable class="parameter">object_name</replaceable> |
+  ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   RULE <replaceable class="parameter">rule_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   SCHEMA <replaceable class="parameter">object_name</replaceable> |
   SEQUENCE <replaceable class="parameter">object_name</replaceable> |
@@ -121,13 +123,15 @@ <title>Parameters</title>
     <term><replaceable class="parameter">function_name</replaceable></term>
     <term><replaceable class="parameter">operator_name</replaceable></term>
     <term><replaceable class="parameter">policy_name</replaceable></term>
+    <term><replaceable class="parameter">procedure_name</replaceable></term>
+    <term><replaceable class="parameter">routine_name</replaceable></term>
     <term><replaceable class="parameter">rule_name</replaceable></term>
     <term><replaceable class="parameter">trigger_name</replaceable></term>
     <listitem>
      <para>
       The name of the object to be commented.  Names of tables,
       aggregates, collations, conversions, domains, foreign tables, functions,
-      indexes, operators, operator classes, operator families, sequences,
+      indexes, operators, operator classes, operator families, procedures, routines, sequences,
       statistics, text search objects, types, and views can be
       schema-qualified. When commenting on a column,
       <replaceable class="parameter">relation_name</replaceable> must refer
@@ -170,7 +174,7 @@ <title>Parameters</title>
     <term><replaceable class="parameter">argmode</replaceable></term>
     <listitem>
      <para>
-      The mode of a function or aggregate
+      The mode of a function, procedure, or aggregate
       argument: <literal>IN</literal>, <literal>OUT</literal>,
       <literal>INOUT</literal>, or <literal>VARIADIC</literal>.
       If omitted, the default is <literal>IN</literal>.
@@ -187,7 +191,7 @@ <title>Parameters</title>
     <term><replaceable class="parameter">argname</replaceable></term>
     <listitem>
      <para>
-      The name of a function or aggregate argument.
+      The name of a function, procedure, or aggregate argument.
       Note that <command>COMMENT</command> does not actually pay
       any attention to argument names, since only the argument data
       types are needed to determine the function's identity.
@@ -199,7 +203,7 @@ <title>Parameters</title>
     <term><replaceable class="parameter">argtype</replaceable></term>
     <listitem>
      <para>
-      The data type of a function or aggregate argument.
+      The data type of a function, procedure, or aggregate argument.
      </para>
     </listitem>
    </varlistentry>
@@ -325,6 +329,7 @@ <title>Examples</title>
 COMMENT ON OPERATOR CLASS int4ops USING btree IS '4 byte integer operators for btrees';
 COMMENT ON OPERATOR FAMILY integer_ops USING btree IS 'all integer operators for btrees';
 COMMENT ON POLICY my_policy ON mytable IS 'Filter rows by users';
+COMMENT ON PROCEDURE my_proc (integer, integer) IS 'Runs a report';
 COMMENT ON ROLE my_role IS 'Administration group for finance tables';
 COMMENT ON RULE my_rule ON my_table IS 'Logs updates of employee records';
 COMMENT ON SCHEMA my_schema IS 'Departmental data';
diff --git a/doc/src/sgml/ref/create_function.sgml b/doc/src/sgml/ref/create_function.sgml
index 75331165fe..fd229d1193 100644
--- a/doc/src/sgml/ref/create_function.sgml
+++ b/doc/src/sgml/ref/create_function.sgml
@@ -55,9 +55,9 @@ <title>Description</title>
   <para>
    If a schema name is included, then the function is created in the
    specified schema.  Otherwise it is created in the current schema.
-   The name of the new function must not match any existing function
+   The name of the new function must not match any existing function or procedure
    with the same input argument types in the same schema.  However,
-   functions of different argument types can share a name (this is
+   functions and procedures of different argument types can share a name (this is
    called <firstterm>overloading</firstterm>).
   </para>
 
@@ -450,7 +450,7 @@ <title>Parameters</title>
    </varlistentry>
 
     <varlistentry>
-     <term><replaceable class="parameter">execution_cost</replaceable></term>
+     <term><literal>COST</literal> <replaceable class="parameter">execution_cost</replaceable></term>
 
      <listitem>
       <para>
@@ -466,7 +466,7 @@ <title>Parameters</title>
     </varlistentry>
 
     <varlistentry>
-     <term><replaceable class="parameter">result_rows</replaceable></term>
+     <term><literal>ROWS</literal> <replaceable class="parameter">result_rows</replaceable></term>
 
      <listitem>
       <para>
@@ -818,7 +818,7 @@ <title>Writing <literal>SECURITY DEFINER</literal> Functions Safely</title>
   <title>Compatibility</title>
 
   <para>
-   A <command>CREATE FUNCTION</command> command is defined in SQL:1999 and later.
+   A <command>CREATE FUNCTION</command> command is defined in the SQL standard.
    The <productname>PostgreSQL</productname> version is similar but
    not fully compatible.  The attributes are not portable, neither are the
    different available languages.
diff --git a/doc/src/sgml/ref/create_procedure.sgml b/doc/src/sgml/ref/create_procedure.sgml
new file mode 100644
index 0000000000..d712043824
--- /dev/null
+++ b/doc/src/sgml/ref/create_procedure.sgml
@@ -0,0 +1,341 @@
+<!--
+doc/src/sgml/ref/create_procedure.sgml
+-->
+
+<refentry id="sql-createprocedure">
+ <indexterm zone="sql-createprocedure">
+  <primary>CREATE PROCEDURE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE PROCEDURE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE PROCEDURE</refname>
+  <refpurpose>define a new procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE [ OR REPLACE ] PROCEDURE
+    <replaceable class="parameter">name</replaceable> ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [ { DEFAULT | = } <replaceable class="parameter">default_expr</replaceable> ] [, ...] ] )
+  { LANGUAGE <replaceable class="parameter">lang_name</replaceable>
+    | TRANSFORM { FOR TYPE <replaceable class="parameter">type_name</replaceable> } [, ... ]
+    | [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
+    | SET <replaceable class="parameter">configuration_parameter</replaceable> { TO <replaceable class="parameter">value</replaceable> | = <replaceable class="parameter">value</replaceable> | FROM CURRENT }
+    | AS '<replaceable class="parameter">definition</replaceable>'
+    | AS '<replaceable class="parameter">obj_file</replaceable>', '<replaceable class="parameter">link_symbol</replaceable>'
+  } ...
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="sql-createprocedure-description">
+  <title>Description</title>
+
+  <para>
+   <command>CREATE PROCEDURE</command> defines a new procedure.
+   <command>CREATE OR REPLACE PROCEDURE</command> will either create a
+   new procedure, or replace an existing definition.
+   To be able to define a procedure, the user must have the
+   <literal>USAGE</literal> privilege on the language.
+  </para>
+
+  <para>
+   If a schema name is included, then the procedure is created in the
+   specified schema.  Otherwise it is created in the current schema.
+   The name of the new procedure must not match any existing procedure or function
+   with the same input argument types in the same schema.  However,
+   procedures and functions of different argument types can share a name (this is
+   called <firstterm>overloading</firstterm>).
+  </para>
+
+  <para>
+   To replace the current definition of an existing procedure, use
+   <command>CREATE OR REPLACE PROCEDURE</command>.  It is not possible
+   to change the name or argument types of a procedure this way (if you
+   tried, you would actually be creating a new, distinct procedure).
+  </para>
+
+  <para>
+   When <command>CREATE OR REPLACE PROCEDURE</command> is used to replace an
+   existing procedure, the ownership and permissions of the procedure
+   do not change.  All other procedure properties are assigned the
+   values specified or implied in the command.  You must own the procedure
+   to replace it (this includes being a member of the owning role).
+  </para>
+
+  <para>
+   The user that creates the procedure becomes the owner of the procedure.
+  </para>
+
+  <para>
+   To be able to create a procedure, you must have <literal>USAGE</literal>
+   privilege on the argument types.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><replaceable class="parameter">name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the procedure to create.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">argmode</replaceable></term>
+
+     <listitem>
+      <para>
+       The mode of an argument: <literal>IN</literal> or <literal>VARIADIC</literal>.
+       If omitted, the default is <literal>IN</literal>.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">argname</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of an argument.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">argtype</replaceable></term>
+
+     <listitem>
+      <para>
+       The data type(s) of the procedure's arguments (optionally
+       schema-qualified), if any. The argument types can be base, composite,
+       or domain types, or can reference the type of a table column.
+      </para>
+      <para>
+       Depending on the implementation language it might also be allowed
+       to specify <quote>pseudo-types</quote> such as <type>cstring</type>.
+       Pseudo-types indicate that the actual argument type is either
+       incompletely specified, or outside the set of ordinary SQL data types.
+      </para>
+      <para>
+       The type of a column is referenced by writing
+       <literal><replaceable
+       class="parameter">table_name</replaceable>.<replaceable
+       class="parameter">column_name</replaceable>%TYPE</literal>.
+       Using this feature can sometimes help make a procedure independent of
+       changes to the definition of a table.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">default_expr</replaceable></term>
+
+     <listitem>
+      <para>
+       An expression to be used as default value if the parameter is
+       not specified.  The expression has to be coercible to the
+       argument type of the parameter.
+       All input parameters following a
+       parameter with a default value must have default values as well.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">lang_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the language that the procedure is implemented in.
+       It can be <literal>sql</literal>, <literal>c</literal>,
+       <literal>internal</literal>, or the name of a user-defined
+       procedural language, e.g. <literal>plpgsql</literal>.  Enclosing the
+       name in single quotes is deprecated and requires matching case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>TRANSFORM { FOR TYPE <replaceable class="parameter">type_name</replaceable> } [, ... ] }</literal></term>
+
+     <listitem>
+      <para>
+       Lists which transforms a call to the procedure should apply.  Transforms
+       convert between SQL types and language-specific data types;
+       see <xref linkend="sql-createtransform"/>.  Procedural language
+       implementations usually have hardcoded knowledge of the built-in types,
+       so those don't need to be listed here.  If a procedural language
+       implementation does not know how to handle a type and no transform is
+       supplied, it will fall back to a default behavior for converting data
+       types, but this depends on the implementation.
+      </para>
+     </listitem>
+    </varlistentry>
+
+   <varlistentry>
+    <term><literal><optional>EXTERNAL</optional> SECURITY INVOKER</literal></term>
+    <term><literal><optional>EXTERNAL</optional> SECURITY DEFINER</literal></term>
+
+    <listitem>
+     <para><literal>SECURITY INVOKER</literal> indicates that the procedure
+      is to be executed with the privileges of the user that calls it.
+      That is the default.  <literal>SECURITY DEFINER</literal>
+      specifies that the procedure is to be executed with the
+      privileges of the user that owns it.
+     </para>
+
+     <para>
+      The key word <literal>EXTERNAL</literal> is allowed for SQL
+      conformance, but it is optional since, unlike in SQL, this feature
+      applies to all procedures not only external ones.
+     </para>
+    </listitem>
+   </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>configuration_parameter</replaceable></term>
+     <term><replaceable>value</replaceable></term>
+     <listitem>
+      <para>
+       The <literal>SET</literal> clause causes the specified configuration
+       parameter to be set to the specified value when the procedure is
+       entered, and then restored to its prior value when the procedure exits.
+       <literal>SET FROM CURRENT</literal> saves the value of the parameter that
+       is current when <command>CREATE PROCEDURE</command> is executed as the value
+       to be applied when the procedure is entered.
+      </para>
+
+      <para>
+       If a <literal>SET</literal> clause is attached to a procedure, then
+       the effects of a <command>SET LOCAL</command> command executed inside the
+       procedure for the same variable are restricted to the procedure: the
+       configuration parameter's prior value is still restored at procedure exit.
+       However, an ordinary
+       <command>SET</command> command (without <literal>LOCAL</literal>) overrides the
+       <literal>SET</literal> clause, much as it would do for a previous <command>SET
+       LOCAL</command> command: the effects of such a command will persist after
+       procedure exit, unless the current transaction is rolled back.
+      </para>
+
+      <para>
+       See <xref linkend="sql-set"/> and
+       <xref linkend="runtime-config"/>
+       for more information about allowed parameter names and values.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable class="parameter">definition</replaceable></term>
+
+     <listitem>
+      <para>
+       A string constant defining the procedure; the meaning depends on the
+       language.  It can be an internal procedure name, the path to an
+       object file, an SQL command, or text in a procedural language.
+      </para>
+
+      <para>
+       It is often helpful to use dollar quoting (see <xref
+       linkend="sql-syntax-dollar-quoting"/>) to write the procedure definition
+       string, rather than the normal single quote syntax.  Without dollar
+       quoting, any single quotes or backslashes in the procedure definition must
+       be escaped by doubling them.
+      </para>
+
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal><replaceable class="parameter">obj_file</replaceable>, <replaceable class="parameter">link_symbol</replaceable></literal></term>
+
+     <listitem>
+      <para>
+       This form of the <literal>AS</literal> clause is used for
+       dynamically loadable C language procedures when the procedure name
+       in the C language source code is not the same as the name of
+       the SQL procedure. The string <replaceable
+       class="parameter">obj_file</replaceable> is the name of the shared
+       library file containing the compiled C procedure, and is interpreted
+       as for the <xref linkend="sql-load"/> command.  The string
+       <replaceable class="parameter">link_symbol</replaceable> is the
+       procedure's link symbol, that is, the name of the procedure in the C
+       language source code.  If the link symbol is omitted, it is assumed
+       to be the same as the name of the SQL procedure being defined.
+      </para>
+
+      <para>
+       When repeated <command>CREATE PROCEDURE</command> calls refer to
+       the same object file, the file is only loaded once per session.
+       To unload and
+       reload the file (perhaps during development), start a new session.
+      </para>
+
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-createprocedure-notes">
+  <title>Notes</title>
+
+  <para>
+   See <xref linkend="sql-createfunction"/> for more details on function
+   creation that also apply to procedures.
+  </para>
+
+  <para>
+   Use <xref linkend="sql-call"/> to execute a procedure.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-createprocedure-examples">
+  <title>Examples</title>
+
+<programlisting>
+CREATE PROCEDURE insert_data(a integer, b integer)
+LANGUAGE SQL
+AS $$
+INSERT INTO tbl VALUES (a);
+INSERT INTO tbl VALUES (b);
+$$;
+
+CALL insert_data(1, 2);
+</programlisting>
+ </refsect1>
+
+ <refsect1 id="sql-createprocedure-compat">
+  <title>Compatibility</title>
+
+  <para>
+   A <command>CREATE PROCEDURE</command> command is defined in the SQL
+   standard.  The <productname>PostgreSQL</productname> version is similar but
+   not fully compatible.  For details see
+   also <xref linkend="sql-createfunction"/>.
+  </para>
+ </refsect1>
+
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterprocedure"/></member>
+   <member><xref linkend="sql-dropprocedure"/></member>
+   <member><xref linkend="sql-call"/></member>
+   <member><xref linkend="sql-createfunction"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_function.sgml b/doc/src/sgml/ref/drop_function.sgml
index eda1a59c84..127fdfe419 100644
--- a/doc/src/sgml/ref/drop_function.sgml
+++ b/doc/src/sgml/ref/drop_function.sgml
@@ -185,6 +185,8 @@ <title>See Also</title>
   <simplelist type="inline">
    <member><xref linkend="sql-createfunction"/></member>
    <member><xref linkend="sql-alterfunction"/></member>
+   <member><xref linkend="sql-dropprocedure"/></member>
+   <member><xref linkend="sql-droproutine"/></member>
   </simplelist>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/drop_procedure.sgml b/doc/src/sgml/ref/drop_procedure.sgml
new file mode 100644
index 0000000000..fef61b66ac
--- /dev/null
+++ b/doc/src/sgml/ref/drop_procedure.sgml
@@ -0,0 +1,162 @@
+<!--
+doc/src/sgml/ref/drop_procedure.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropprocedure">
+ <indexterm zone="sql-dropprocedure">
+  <primary>DROP PROCEDURE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP PROCEDURE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP PROCEDURE</refname>
+  <refpurpose>remove a procedure</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP PROCEDURE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] [, ...]
+    [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP PROCEDURE</command> removes the definition of an existing
+   procedure. To execute this command the user must be the
+   owner of the procedure. The argument types to the
+   procedure must be specified, since several different procedures
+   can exist with the same name and different argument lists.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+    <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the procedure does not exist. A notice is issued
+      in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+  <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing procedure.  If no
+      argument list is specified, the name must be unique in its schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argmode</replaceable></term>
+
+    <listitem>
+     <para>
+      The mode of an argument: <literal>IN</literal> or <literal>VARIADIC</literal>.
+      If omitted, the default is <literal>IN</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argname</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of an argument.
+      Note that <command>DROP PROCEDURE</command> does not actually pay
+      any attention to argument names, since only the argument data
+      types are needed to determine the procedure's identity.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">argtype</replaceable></term>
+
+    <listitem>
+     <para>
+      The data type(s) of the procedure's arguments (optionally
+      schema-qualified), if any.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the procedure,
+      and in turn all objects that depend on those objects
+      (see <xref linkend="ddl-depend"/>).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the procedure if any objects depend on it.  This
+      is the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-dropprocedure-examples">
+  <title>Examples</title>
+
+<programlisting>
+DROP PROCEDURE do_db_maintenance();
+</programlisting>
+ </refsect1>
+
+ <refsect1 id="sql-dropprocedure-compatibility">
+  <title>Compatibility</title>
+
+  <para>
+   This command conforms to the SQL standard, with
+   these <productname>PostgreSQL</productname> extensions:
+   <itemizedlist>
+    <listitem>
+     <para>The standard only allows one procedure to be dropped per command.</para>
+    </listitem>
+    <listitem>
+     <para>The <literal>IF EXISTS</literal> option</para>
+    </listitem>
+    <listitem>
+     <para>The ability to specify argument modes and names</para>
+    </listitem>
+   </itemizedlist>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createprocedure"/></member>
+   <member><xref linkend="sql-alterprocedure"/></member>
+   <member><xref linkend="sql-dropfunction"/></member>
+   <member><xref linkend="sql-droproutine"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_routine.sgml b/doc/src/sgml/ref/drop_routine.sgml
new file mode 100644
index 0000000000..5cd1a0f11e
--- /dev/null
+++ b/doc/src/sgml/ref/drop_routine.sgml
@@ -0,0 +1,94 @@
+<!--
+doc/src/sgml/ref/drop_routine.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-droproutine">
+ <indexterm zone="sql-droproutine">
+  <primary>DROP ROUTINE</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP ROUTINE</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP ROUTINE</refname>
+  <refpurpose>remove a routine</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP ROUTINE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] [, ...]
+    [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP ROUTINE</command> removes the definition of an existing
+   routine, which can be an aggregate function, a normal function, or a
+   procedure.  See
+   under <xref linkend="sql-dropaggregate"/>, <xref linkend="sql-dropfunction"/>,
+   and <xref linkend="sql-dropprocedure"/> for the description of the
+   parameters, more examples, and further details.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-droproutine-examples">
+  <title>Examples</title>
+
+  <para>
+   To drop the routine <literal>foo</literal> for type
+   <type>integer</type>:
+<programlisting>
+DROP ROUTINE foo(integer);
+</programlisting>
+   This command will work independent of whether <literal>foo</literal> is an
+   aggregate, function, or procedure.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-droproutine-compatibility">
+  <title>Compatibility</title>
+
+  <para>
+   This command conforms to the SQL standard, with
+   these <productname>PostgreSQL</productname> extensions:
+   <itemizedlist>
+    <listitem>
+     <para>The standard only allows one routine to be dropped per command.</para>
+    </listitem>
+    <listitem>
+     <para>The <literal>IF EXISTS</literal> option</para>
+    </listitem>
+    <listitem>
+     <para>The ability to specify argument modes and names</para>
+    </listitem>
+    <listitem>
+     <para>Aggregate functions are an extension.</para>
+    </listitem>
+   </itemizedlist>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-dropaggregate"/></member>
+   <member><xref linkend="sql-dropfunction"/></member>
+   <member><xref linkend="sql-dropprocedure"/></member>
+   <member><xref linkend="sql-alterroutine"/></member>
+  </simplelist>
+
+  <para>
+   Note that there is no <literal>CREATE ROUTINE</literal> command.
+  </para>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a5e895d09d..ff64c7a3ba 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -55,8 +55,8 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
 
 GRANT { EXECUTE | ALL [ PRIVILEGES ] }
-    ON { FUNCTION <replaceable>function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
-         | ALL FUNCTIONS IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] }
+    ON { { FUNCTION | PROCEDURE | ROUTINE } <replaceable>routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
+         | ALL { FUNCTIONS | PROCEDURES | ROUTINES } IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] }
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
 
 GRANT { USAGE | ALL [ PRIVILEGES ] }
@@ -96,7 +96,7 @@ <title>Description</title>
   <para>
    The <command>GRANT</command> command has two basic variants: one
    that grants privileges on a database object (table, column, view, foreign
-   table, sequence, database, foreign-data wrapper, foreign server, function,
+   table, sequence, database, foreign-data wrapper, foreign server, function, procedure,
    procedural language, schema, or tablespace), and one that grants
    membership in a role.  These variants are similar in many ways, but
    they are different enough to be described separately.
@@ -115,8 +115,11 @@ <title>GRANT on Database Objects</title>
   <para>
    There is also an option to grant privileges on all objects of the same
    type within one or more schemas.  This functionality is currently supported
-   only for tables, sequences, and functions (but note that <literal>ALL
-   TABLES</literal> is considered to include views and foreign tables).
+   only for tables, sequences, functions, and procedures.  <literal>ALL
+   TABLES</literal> also affects views and foreign tables, just like the
+   specific-object <command>GRANT</command> command.  <literal>ALL
+   FUNCTIONS</literal> also affects aggregate functions, but not procedures,
+   again just like the specific-object <command>GRANT</command> command.
   </para>
 
   <para>
@@ -169,7 +172,7 @@ <title>GRANT on Database Objects</title>
    granted to <literal>PUBLIC</literal> are as follows:
    <literal>CONNECT</literal> and <literal>TEMPORARY</literal> (create
    temporary tables) privileges for databases;
-   <literal>EXECUTE</literal> privilege for functions; and
+   <literal>EXECUTE</literal> privilege for functions and procedures; and
    <literal>USAGE</literal> privilege for languages and data types
    (including domains).
    The object owner can, of course, <command>REVOKE</command>
@@ -329,10 +332,12 @@ <title>GRANT on Database Objects</title>
      <term><literal>EXECUTE</literal></term>
      <listitem>
       <para>
-       Allows the use of the specified function and the use of any
-       operators that are implemented on top of the function.  This is
-       the only type of privilege that is applicable to functions.
-       (This syntax works for aggregate functions, as well.)
+       Allows the use of the specified function or procedure and the use of
+       any operators that are implemented on top of the function.  This is the
+       only type of privilege that is applicable to functions and procedures.
+       The <literal>FUNCTION</literal> syntax also works for aggregate
+       functions.  Alternatively, use <literal>ROUTINE</literal> to refer to a function,
+       aggregate function, or procedure regardless of what it is.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 4d133a782b..7018202f14 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -70,8 +70,8 @@
 
 REVOKE [ GRANT OPTION FOR ]
     { EXECUTE | ALL [ PRIVILEGES ] }
-    ON { FUNCTION <replaceable>function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
-         | ALL FUNCTIONS IN SCHEMA <replaceable>schema_name</replaceable> [, ...] }
+    ON { { FUNCTION | PROCEDURE | ROUTINE } <replaceable>function_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">arg_name</replaceable> ] <replaceable class="parameter">arg_type</replaceable> [, ...] ] ) ] [, ...]
+         | ALL { FUNCTIONS | PROCEDURES | ROUTINES } IN SCHEMA <replaceable>schema_name</replaceable> [, ...] }
     FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...]
     [ CASCADE | RESTRICT ]
 
diff --git a/doc/src/sgml/ref/security_label.sgml b/doc/src/sgml/ref/security_label.sgml
index d52113e035..e9cfdec9f9 100644
--- a/doc/src/sgml/ref/security_label.sgml
+++ b/doc/src/sgml/ref/security_label.sgml
@@ -34,8 +34,10 @@
   LARGE OBJECT <replaceable class="parameter">large_object_oid</replaceable> |
   MATERIALIZED VIEW <replaceable class="parameter">object_name</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
+  PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   PUBLICATION <replaceable class="parameter">object_name</replaceable> |
   ROLE <replaceable class="parameter">object_name</replaceable> |
+  ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   SCHEMA <replaceable class="parameter">object_name</replaceable> |
   SEQUENCE <replaceable class="parameter">object_name</replaceable> |
   SUBSCRIPTION <replaceable class="parameter">object_name</replaceable> |
@@ -93,10 +95,12 @@ <title>Parameters</title>
     <term><replaceable class="parameter">table_name.column_name</replaceable></term>
     <term><replaceable class="parameter">aggregate_name</replaceable></term>
     <term><replaceable class="parameter">function_name</replaceable></term>
+    <term><replaceable class="parameter">procedure_name</replaceable></term>
+    <term><replaceable class="parameter">routine_name</replaceable></term>
     <listitem>
      <para>
       The name of the object to be labeled.  Names of tables,
-      aggregates, domains, foreign tables, functions, sequences, types, and
+      aggregates, domains, foreign tables, functions, procedures, routines, sequences, types, and
       views can be schema-qualified.
      </para>
     </listitem>
@@ -119,7 +123,7 @@ <title>Parameters</title>
 
     <listitem>
      <para>
-      The mode of a function or aggregate
+      The mode of a function, procedure, or aggregate
       argument: <literal>IN</literal>, <literal>OUT</literal>,
       <literal>INOUT</literal>, or <literal>VARIADIC</literal>.
       If omitted, the default is <literal>IN</literal>.
@@ -137,7 +141,7 @@ <title>Parameters</title>
 
     <listitem>
      <para>
-      The name of a function or aggregate argument.
+      The name of a function, procedure, or aggregate argument.
       Note that <command>SECURITY LABEL</command> does not actually
       pay any attention to argument names, since only the argument data
       types are needed to determine the function's identity.
@@ -150,7 +154,7 @@ <title>Parameters</title>
 
     <listitem>
      <para>
-      The data type of a function or aggregate argument.
+      The data type of a function, procedure, or aggregate argument.
      </para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index d20eaa87e7..d27fb414f7 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -54,8 +54,10 @@ <title>SQL Commands</title>
    &alterOperatorClass;
    &alterOperatorFamily;
    &alterPolicy;
+   &alterProcedure;
    &alterPublication;
    &alterRole;
+   &alterRoutine;
    &alterRule;
    &alterSchema;
    &alterSequence;
@@ -76,6 +78,7 @@ <title>SQL Commands</title>
    &alterView;
    &analyze;
    &begin;
+   &call;
    &checkpoint;
    &close;
    &cluster;
@@ -103,6 +106,7 @@ <title>SQL Commands</title>
    &createOperatorClass;
    &createOperatorFamily;
    &createPolicy;
+   &createProcedure;
    &createPublication;
    &createRole;
    &createRule;
@@ -150,8 +154,10 @@ <title>SQL Commands</title>
    &dropOperatorFamily;
    &dropOwned;
    &dropPolicy;
+   &dropProcedure;
    &dropPublication;
    &dropRole;
+   &dropRoutine;
    &dropRule;
    &dropSchema;
    &dropSequence;
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 508ee7a96c..bbc3766cc2 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -72,6 +72,39 @@ <title>User-defined Functions</title>
   </para>
   </sect1>
 
+  <sect1 id="xproc">
+   <title>User-defined Procedures</title>
+
+  <indexterm zone="xproc">
+   <primary>procedure</primary>
+   <secondary>user-defined</secondary>
+  </indexterm>
+
+   <para>
+    A procedure is a database object similar to a function.  The difference is
+    that a procedure does not return a value, so there is no return type
+    declaration.  While a function is called as part of a query or DML
+    command, a procedure is called explicitly using
+    the <xref linkend="sql-call"/> statement.
+   </para>
+
+   <para>
+    The explanations on how to define user-defined functions in the rest of
+    this chapter apply to procedures as well, except that
+    the <xref linkend="sql-createprocedure"/> command is used instead, there is
+    no return type, and some other features such as strictness don't apply.
+   </para>
+
+   <para>
+    Collectively, functions and procedures are also known
+    as <firstterm>routines</firstterm><indexterm><primary>routine</primary></indexterm>.
+    There are commands such as <xref linkend="sql-alterroutine"/>
+    and <xref linkend="sql-droproutine"/> that can operate on functions and
+    procedures without having to know which kind it is.  Note, however, that
+    there is no <literal>CREATE ROUTINE</literal> command.
+   </para>
+  </sect1>
+
   <sect1 id="xfunc-sql">
    <title>Query Language (<acronym>SQL</acronym>) Functions</title>
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ccde66a7dd..e481cf3d11 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -482,6 +482,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_NAMESPACE;
 			errormsg = gettext_noop("invalid privilege type %s for schema");
 			break;
+		case ACL_OBJECT_PROCEDURE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for procedure");
+			break;
+		case ACL_OBJECT_ROUTINE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for routine");
+			break;
 		case ACL_OBJECT_TABLESPACE:
 			all_privileges = ACL_ALL_RIGHTS_TABLESPACE;
 			errormsg = gettext_noop("invalid privilege type %s for tablespace");
@@ -584,6 +592,8 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 			ExecGrant_ForeignServer(istmt);
 			break;
 		case ACL_OBJECT_FUNCTION:
+		case ACL_OBJECT_PROCEDURE:
+		case ACL_OBJECT_ROUTINE:
 			ExecGrant_Function(istmt);
 			break;
 		case ACL_OBJECT_LANGUAGE:
@@ -671,7 +681,7 @@ objectNamesToOids(GrantObjectType objtype, List *objnames)
 				ObjectWithArgs *func = (ObjectWithArgs *) lfirst(cell);
 				Oid			funcid;
 
-				funcid = LookupFuncWithArgs(func, false);
+				funcid = LookupFuncWithArgs(OBJECT_FUNCTION, func, false);
 				objects = lappend_oid(objects, funcid);
 			}
 			break;
@@ -709,6 +719,26 @@ objectNamesToOids(GrantObjectType objtype, List *objnames)
 				objects = lappend_oid(objects, oid);
 			}
 			break;
+		case ACL_OBJECT_PROCEDURE:
+			foreach(cell, objnames)
+			{
+				ObjectWithArgs *func = (ObjectWithArgs *) lfirst(cell);
+				Oid			procid;
+
+				procid = LookupFuncWithArgs(OBJECT_PROCEDURE, func, false);
+				objects = lappend_oid(objects, procid);
+			}
+			break;
+		case ACL_OBJECT_ROUTINE:
+			foreach(cell, objnames)
+			{
+				ObjectWithArgs *func = (ObjectWithArgs *) lfirst(cell);
+				Oid			routid;
+
+				routid = LookupFuncWithArgs(OBJECT_ROUTINE, func, false);
+				objects = lappend_oid(objects, routid);
+			}
+			break;
 		case ACL_OBJECT_TABLESPACE:
 			foreach(cell, objnames)
 			{
@@ -785,19 +815,39 @@ objectsInSchemaToOids(GrantObjectType objtype, List *nspnames)
 				objects = list_concat(objects, objs);
 				break;
 			case ACL_OBJECT_FUNCTION:
+			case ACL_OBJECT_PROCEDURE:
+			case ACL_OBJECT_ROUTINE:
 				{
-					ScanKeyData key[1];
+					ScanKeyData key[2];
+					int			keycount;
 					Relation	rel;
 					HeapScanDesc scan;
 					HeapTuple	tuple;
 
-					ScanKeyInit(&key[0],
+					keycount = 0;
+					ScanKeyInit(&key[keycount++],
 								Anum_pg_proc_pronamespace,
 								BTEqualStrategyNumber, F_OIDEQ,
 								ObjectIdGetDatum(namespaceId));
 
+					/*
+					 * When looking for functions, check for return type <>0.
+					 * When looking for procedures, check for return type ==0.
+					 * When looking for routines, don't check the return type.
+					 */
+					if (objtype == ACL_OBJECT_FUNCTION)
+						ScanKeyInit(&key[keycount++],
+									Anum_pg_proc_prorettype,
+									BTEqualStrategyNumber, F_OIDNE,
+									InvalidOid);
+					else if (objtype == ACL_OBJECT_PROCEDURE)
+						ScanKeyInit(&key[keycount++],
+									Anum_pg_proc_prorettype,
+									BTEqualStrategyNumber, F_OIDEQ,
+									InvalidOid);
+
 					rel = heap_open(ProcedureRelationId, AccessShareLock);
-					scan = heap_beginscan_catalog(rel, 1, key);
+					scan = heap_beginscan_catalog(rel, keycount, key);
 
 					while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
 					{
@@ -955,6 +1005,14 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
 			errormsg = gettext_noop("invalid privilege type %s for function");
 			break;
+		case ACL_OBJECT_PROCEDURE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for procedure");
+			break;
+		case ACL_OBJECT_ROUTINE:
+			all_privileges = ACL_ALL_RIGHTS_FUNCTION;
+			errormsg = gettext_noop("invalid privilege type %s for routine");
+			break;
 		case ACL_OBJECT_TYPE:
 			all_privileges = ACL_ALL_RIGHTS_TYPE;
 			errormsg = gettext_noop("invalid privilege type %s for type");
@@ -1423,7 +1481,7 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 				istmt.objtype = ACL_OBJECT_TYPE;
 				break;
 			case ProcedureRelationId:
-				istmt.objtype = ACL_OBJECT_FUNCTION;
+				istmt.objtype = ACL_OBJECT_ROUTINE;
 				break;
 			case LanguageRelationId:
 				istmt.objtype = ACL_OBJECT_LANGUAGE;
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 236f6be37e..360725d59a 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1413,7 +1413,8 @@ CREATE VIEW routines AS
            CAST(current_database() AS sql_identifier) AS routine_catalog,
            CAST(n.nspname AS sql_identifier) AS routine_schema,
            CAST(p.proname AS sql_identifier) AS routine_name,
-           CAST('FUNCTION' AS character_data) AS routine_type,
+           CAST(CASE WHEN p.prorettype <> 0 THEN 'FUNCTION' ELSE 'PROCEDURE' END
+             AS character_data) AS routine_type,
            CAST(null AS sql_identifier) AS module_catalog,
            CAST(null AS sql_identifier) AS module_schema,
            CAST(null AS sql_identifier) AS module_name,
@@ -1422,7 +1423,8 @@ CREATE VIEW routines AS
            CAST(null AS sql_identifier) AS udt_name,
 
            CAST(
-             CASE WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY'
+             CASE WHEN p.prorettype = 0 THEN NULL
+                  WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY'
                   WHEN nt.nspname = 'pg_catalog' THEN format_type(t.oid, null)
                   ELSE 'USER-DEFINED' END AS character_data)
              AS data_type,
@@ -1440,7 +1442,7 @@ CREATE VIEW routines AS
            CAST(null AS cardinal_number) AS datetime_precision,
            CAST(null AS character_data) AS interval_type,
            CAST(null AS cardinal_number) AS interval_precision,
-           CAST(current_database() AS sql_identifier) AS type_udt_catalog,
+           CAST(CASE WHEN p.prorettype <> 0 THEN current_database() END AS sql_identifier) AS type_udt_catalog,
            CAST(nt.nspname AS sql_identifier) AS type_udt_schema,
            CAST(t.typname AS sql_identifier) AS type_udt_name,
            CAST(null AS sql_identifier) AS scope_catalog,
@@ -1462,7 +1464,8 @@ CREATE VIEW routines AS
            CAST('GENERAL' AS character_data) AS parameter_style,
            CAST(CASE WHEN p.provolatile = 'i' THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_deterministic,
            CAST('MODIFIES' AS character_data) AS sql_data_access,
-           CAST(CASE WHEN p.proisstrict THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_null_call,
+           CAST(CASE WHEN p.prorettype <> 0 THEN
+             CASE WHEN p.proisstrict THEN 'YES' ELSE 'NO' END END AS yes_or_no) AS is_null_call,
            CAST(null AS character_data) AS sql_path,
            CAST('YES' AS yes_or_no) AS schema_level_routine,
            CAST(0 AS cardinal_number) AS max_dynamic_result_sets,
@@ -1503,13 +1506,15 @@ CREATE VIEW routines AS
            CAST(null AS cardinal_number) AS result_cast_maximum_cardinality,
            CAST(null AS sql_identifier) AS result_cast_dtd_identifier
 
-    FROM pg_namespace n, pg_proc p, pg_language l,
-         pg_type t, pg_namespace nt
+    FROM (pg_namespace n
+          JOIN pg_proc p ON n.oid = p.pronamespace
+          JOIN pg_language l ON p.prolang = l.oid)
+         LEFT JOIN
+         (pg_type t JOIN pg_namespace nt ON t.typnamespace = nt.oid)
+         ON p.prorettype = t.oid
 
-    WHERE n.oid = p.pronamespace AND p.prolang = l.oid
-          AND p.prorettype = t.oid AND t.typnamespace = nt.oid
-          AND (pg_has_role(p.proowner, 'USAGE')
-               OR has_function_privilege(p.oid, 'EXECUTE'));
+    WHERE (pg_has_role(p.proowner, 'USAGE')
+           OR has_function_privilege(p.oid, 'EXECUTE'));
 
 GRANT SELECT ON routines TO PUBLIC;
 
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 8d55c76fc4..9553675975 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -566,6 +566,9 @@ static const struct object_type_map
 	{
 		"function", OBJECT_FUNCTION
 	},
+	{
+		"procedure", OBJECT_PROCEDURE
+	},
 	/* OCLASS_TYPE */
 	{
 		"type", OBJECT_TYPE
@@ -884,13 +887,11 @@ get_object_address(ObjectType objtype, Node *object,
 				address = get_object_address_type(objtype, castNode(TypeName, object), missing_ok);
 				break;
 			case OBJECT_AGGREGATE:
-				address.classId = ProcedureRelationId;
-				address.objectId = LookupAggWithArgs(castNode(ObjectWithArgs, object), missing_ok);
-				address.objectSubId = 0;
-				break;
 			case OBJECT_FUNCTION:
+			case OBJECT_PROCEDURE:
+			case OBJECT_ROUTINE:
 				address.classId = ProcedureRelationId;
-				address.objectId = LookupFuncWithArgs(castNode(ObjectWithArgs, object), missing_ok);
+				address.objectId = LookupFuncWithArgs(objtype, castNode(ObjectWithArgs, object), missing_ok);
 				address.objectSubId = 0;
 				break;
 			case OBJECT_OPERATOR:
@@ -2025,6 +2026,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 	 */
 	if (type == OBJECT_AGGREGATE ||
 		type == OBJECT_FUNCTION ||
+		type == OBJECT_PROCEDURE ||
+		type == OBJECT_ROUTINE ||
 		type == OBJECT_OPERATOR ||
 		type == OBJECT_CAST ||
 		type == OBJECT_AMOP ||
@@ -2168,6 +2171,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_AGGREGATE:
 		case OBJECT_OPERATOR:
 			{
@@ -2253,6 +2258,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AGGREGATE:
 		case OBJECT_FUNCTION:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 			if (!pg_proc_ownercheck(address.objectId, roleid))
 				aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_PROC,
 							   NameListToString((castNode(ObjectWithArgs, object))->objname));
@@ -4026,6 +4033,8 @@ getProcedureTypeDescription(StringInfo buffer, Oid procid)
 
 	if (procForm->proisagg)
 		appendStringInfoString(buffer, "aggregate");
+	else if (procForm->prorettype == InvalidOid)
+		appendStringInfoString(buffer, "procedure");
 	else
 		appendStringInfoString(buffer, "function");
 
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 47916cfb54..7d05e4bdb2 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -857,7 +857,8 @@ fmgr_sql_validator(PG_FUNCTION_ARGS)
 
 	/* Disallow pseudotype result */
 	/* except for RECORD, VOID, or polymorphic */
-	if (get_typtype(proc->prorettype) == TYPTYPE_PSEUDO &&
+	if (proc->prorettype &&
+		get_typtype(proc->prorettype) == TYPTYPE_PSEUDO &&
 		proc->prorettype != RECORDOID &&
 		proc->prorettype != VOIDOID &&
 		!IsPolymorphicType(proc->prorettype))
diff --git a/src/backend/commands/aggregatecmds.c b/src/backend/commands/aggregatecmds.c
index adc9877e79..2e2ee883e2 100644
--- a/src/backend/commands/aggregatecmds.c
+++ b/src/backend/commands/aggregatecmds.c
@@ -307,7 +307,7 @@ DefineAggregate(ParseState *pstate, List *name, List *args, bool oldstyle, List
 		interpret_function_parameter_list(pstate,
 										  args,
 										  InvalidOid,
-										  true, /* is an aggregate */
+										  OBJECT_AGGREGATE,
 										  &parameterTypes,
 										  &allParameterTypes,
 										  &parameterModes,
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 4f8147907c..21e3f1efe1 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -378,6 +378,8 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
 		case OBJECT_LANGUAGE:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TSCONFIGURATION:
 		case OBJECT_TSDICTIONARY:
@@ -495,6 +497,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_OPERATOR:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TSCONFIGURATION:
 		case OBJECT_TSDICTIONARY:
@@ -842,6 +846,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_OPERATOR:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
+		case OBJECT_PROCEDURE:
+		case OBJECT_ROUTINE:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TABLESPACE:
 		case OBJECT_TSDICTIONARY:
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 2b30677d6f..7e6baa1928 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -26,6 +26,7 @@
 #include "nodes/makefuncs.h"
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 
@@ -91,21 +92,12 @@ RemoveObjects(DropStmt *stmt)
 		 */
 		if (stmt->removeType == OBJECT_FUNCTION)
 		{
-			Oid			funcOid = address.objectId;
-			HeapTuple	tup;
-
-			tup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcOid));
-			if (!HeapTupleIsValid(tup)) /* should not happen */
-				elog(ERROR, "cache lookup failed for function %u", funcOid);
-
-			if (((Form_pg_proc) GETSTRUCT(tup))->proisagg)
+			if (get_func_isagg(address.objectId))
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 						 errmsg("\"%s\" is an aggregate function",
 								NameListToString(castNode(ObjectWithArgs, object)->objname)),
 						 errhint("Use DROP AGGREGATE to drop aggregate functions.")));
-
-			ReleaseSysCache(tup);
 		}
 
 		/* Check permissions. */
@@ -338,6 +330,32 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				}
 				break;
 			}
+		case OBJECT_PROCEDURE:
+			{
+				ObjectWithArgs *owa = castNode(ObjectWithArgs, object);
+
+				if (!schema_does_not_exist_skipping(owa->objname, &msg, &name) &&
+					!type_in_list_does_not_exist_skipping(owa->objargs, &msg, &name))
+				{
+					msg = gettext_noop("procedure %s(%s) does not exist, skipping");
+					name = NameListToString(owa->objname);
+					args = TypeNameListToString(owa->objargs);
+				}
+				break;
+			}
+		case OBJECT_ROUTINE:
+			{
+				ObjectWithArgs *owa = castNode(ObjectWithArgs, object);
+
+				if (!schema_does_not_exist_skipping(owa->objname, &msg, &name) &&
+					!type_in_list_does_not_exist_skipping(owa->objargs, &msg, &name))
+				{
+					msg = gettext_noop("routine %s(%s) does not exist, skipping");
+					name = NameListToString(owa->objname);
+					args = TypeNameListToString(owa->objargs);
+				}
+				break;
+			}
 		case OBJECT_AGGREGATE:
 			{
 				ObjectWithArgs *owa = castNode(ObjectWithArgs, object);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index fa7d0d015a..a602c20b41 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -106,8 +106,10 @@ static event_trigger_support_data event_trigger_support[] = {
 	{"OPERATOR CLASS", true},
 	{"OPERATOR FAMILY", true},
 	{"POLICY", true},
+	{"PROCEDURE", true},
 	{"PUBLICATION", true},
 	{"ROLE", false},
+	{"ROUTINE", true},
 	{"RULE", true},
 	{"SCHEMA", true},
 	{"SEQUENCE", true},
@@ -1103,8 +1105,10 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_OPERATOR:
 		case OBJECT_OPFAMILY:
 		case OBJECT_POLICY:
+		case OBJECT_PROCEDURE:
 		case OBJECT_PUBLICATION:
 		case OBJECT_PUBLICATION_REL:
+		case OBJECT_ROUTINE:
 		case OBJECT_RULE:
 		case OBJECT_SCHEMA:
 		case OBJECT_SEQUENCE:
@@ -1215,6 +1219,8 @@ EventTriggerSupportsGrantObjectType(GrantObjectType objtype)
 		case ACL_OBJECT_LANGUAGE:
 		case ACL_OBJECT_LARGEOBJECT:
 		case ACL_OBJECT_NAMESPACE:
+		case ACL_OBJECT_PROCEDURE:
+		case ACL_OBJECT_ROUTINE:
 		case ACL_OBJECT_TYPE:
 			return true;
 
@@ -2243,6 +2249,10 @@ stringify_grantobjtype(GrantObjectType objtype)
 			return "LARGE OBJECT";
 		case ACL_OBJECT_NAMESPACE:
 			return "SCHEMA";
+		case ACL_OBJECT_PROCEDURE:
+			return "PROCEDURE";
+		case ACL_OBJECT_ROUTINE:
+			return "ROUTINE";
 		case ACL_OBJECT_TABLESPACE:
 			return "TABLESPACE";
 		case ACL_OBJECT_TYPE:
@@ -2285,6 +2295,10 @@ stringify_adefprivs_objtype(GrantObjectType objtype)
 			return "LARGE OBJECTS";
 		case ACL_OBJECT_NAMESPACE:
 			return "SCHEMAS";
+		case ACL_OBJECT_PROCEDURE:
+			return "PROCEDURES";
+		case ACL_OBJECT_ROUTINE:
+			return "ROUTINES";
 		case ACL_OBJECT_TABLESPACE:
 			return "TABLESPACES";
 		case ACL_OBJECT_TYPE:
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index 7de844b2ca..2a9c90133d 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -51,6 +51,8 @@
 #include "commands/alter.h"
 #include "commands/defrem.h"
 #include "commands/proclang.h"
+#include "executor/execdesc.h"
+#include "executor/executor.h"
 #include "miscadmin.h"
 #include "optimizer/var.h"
 #include "parser/parse_coerce.h"
@@ -179,7 +181,7 @@ void
 interpret_function_parameter_list(ParseState *pstate,
 								  List *parameters,
 								  Oid languageOid,
-								  bool is_aggregate,
+								  ObjectType objtype,
 								  oidvector **parameterTypes,
 								  ArrayType **allParameterTypes,
 								  ArrayType **parameterModes,
@@ -233,7 +235,7 @@ interpret_function_parameter_list(ParseState *pstate,
 							 errmsg("SQL function cannot accept shell type %s",
 									TypeNameToString(t))));
 				/* We don't allow creating aggregates on shell types either */
-				else if (is_aggregate)
+				else if (objtype == OBJECT_AGGREGATE)
 					ereport(ERROR,
 							(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 							 errmsg("aggregate cannot accept shell type %s",
@@ -262,16 +264,28 @@ interpret_function_parameter_list(ParseState *pstate,
 
 		if (t->setof)
 		{
-			if (is_aggregate)
+			if (objtype == OBJECT_AGGREGATE)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 						 errmsg("aggregates cannot accept set arguments")));
+			else if (objtype == OBJECT_PROCEDURE)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+						 errmsg("procedures cannot accept set arguments")));
 			else
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 						 errmsg("functions cannot accept set arguments")));
 		}
 
+		if (objtype == OBJECT_PROCEDURE)
+		{
+			if (fp->mode == FUNC_PARAM_OUT || fp->mode == FUNC_PARAM_INOUT)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 (errmsg("procedures cannot have OUT parameters"))));
+		}
+
 		/* handle input parameters */
 		if (fp->mode != FUNC_PARAM_OUT && fp->mode != FUNC_PARAM_TABLE)
 		{
@@ -451,6 +465,7 @@ interpret_function_parameter_list(ParseState *pstate,
  */
 static bool
 compute_common_attribute(ParseState *pstate,
+						 bool is_procedure,
 						 DefElem *defel,
 						 DefElem **volatility_item,
 						 DefElem **strict_item,
@@ -463,6 +478,8 @@ compute_common_attribute(ParseState *pstate,
 {
 	if (strcmp(defel->defname, "volatility") == 0)
 	{
+		if (is_procedure)
+			goto procedure_error;
 		if (*volatility_item)
 			goto duplicate_error;
 
@@ -470,6 +487,8 @@ compute_common_attribute(ParseState *pstate,
 	}
 	else if (strcmp(defel->defname, "strict") == 0)
 	{
+		if (is_procedure)
+			goto procedure_error;
 		if (*strict_item)
 			goto duplicate_error;
 
@@ -484,6 +503,8 @@ compute_common_attribute(ParseState *pstate,
 	}
 	else if (strcmp(defel->defname, "leakproof") == 0)
 	{
+		if (is_procedure)
+			goto procedure_error;
 		if (*leakproof_item)
 			goto duplicate_error;
 
@@ -495,6 +516,8 @@ compute_common_attribute(ParseState *pstate,
 	}
 	else if (strcmp(defel->defname, "cost") == 0)
 	{
+		if (is_procedure)
+			goto procedure_error;
 		if (*cost_item)
 			goto duplicate_error;
 
@@ -502,6 +525,8 @@ compute_common_attribute(ParseState *pstate,
 	}
 	else if (strcmp(defel->defname, "rows") == 0)
 	{
+		if (is_procedure)
+			goto procedure_error;
 		if (*rows_item)
 			goto duplicate_error;
 
@@ -509,6 +534,8 @@ compute_common_attribute(ParseState *pstate,
 	}
 	else if (strcmp(defel->defname, "parallel") == 0)
 	{
+		if (is_procedure)
+			goto procedure_error;
 		if (*parallel_item)
 			goto duplicate_error;
 
@@ -526,6 +553,13 @@ compute_common_attribute(ParseState *pstate,
 			 errmsg("conflicting or redundant options"),
 			 parser_errposition(pstate, defel->location)));
 	return false;				/* keep compiler quiet */
+
+procedure_error:
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+			 errmsg("invalid attribute in procedure definition"),
+			 parser_errposition(pstate, defel->location)));
+	return false;
 }
 
 static char
@@ -603,6 +637,7 @@ update_proconfig_value(ArrayType *a, List *set_items)
  */
 static void
 compute_attributes_sql_style(ParseState *pstate,
+							 bool is_procedure,
 							 List *options,
 							 List **as,
 							 char **language,
@@ -669,9 +704,15 @@ compute_attributes_sql_style(ParseState *pstate,
 						(errcode(ERRCODE_SYNTAX_ERROR),
 						 errmsg("conflicting or redundant options"),
 						 parser_errposition(pstate, defel->location)));
+			if (is_procedure)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+						 errmsg("invalid attribute in procedure definition"),
+						 parser_errposition(pstate, defel->location)));
 			windowfunc_item = defel;
 		}
 		else if (compute_common_attribute(pstate,
+										  is_procedure,
 										  defel,
 										  &volatility_item,
 										  &strict_item,
@@ -762,7 +803,7 @@ compute_attributes_sql_style(ParseState *pstate,
  *------------
  */
 static void
-compute_attributes_with_style(ParseState *pstate, List *parameters, bool *isStrict_p, char *volatility_p)
+compute_attributes_with_style(ParseState *pstate, bool is_procedure, List *parameters, bool *isStrict_p, char *volatility_p)
 {
 	ListCell   *pl;
 
@@ -771,10 +812,22 @@ compute_attributes_with_style(ParseState *pstate, List *parameters, bool *isStri
 		DefElem    *param = (DefElem *) lfirst(pl);
 
 		if (pg_strcasecmp(param->defname, "isstrict") == 0)
+		{
+			if (is_procedure)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+						 errmsg("invalid attribute in procedure definition"),
+						 parser_errposition(pstate, param->location)));
 			*isStrict_p = defGetBoolean(param);
+		}
 		else if (pg_strcasecmp(param->defname, "iscachable") == 0)
 		{
 			/* obsolete spelling of isImmutable */
+			if (is_procedure)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
+						 errmsg("invalid attribute in procedure definition"),
+						 parser_errposition(pstate, param->location)));
 			if (defGetBoolean(param))
 				*volatility_p = PROVOLATILE_IMMUTABLE;
 		}
@@ -916,6 +969,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 
 	/* override attributes from explicit list */
 	compute_attributes_sql_style(pstate,
+								 stmt->is_procedure,
 								 stmt->options,
 								 &as_clause, &language, &transformDefElem,
 								 &isWindowFunc, &volatility,
@@ -990,7 +1044,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 	interpret_function_parameter_list(pstate,
 									  stmt->parameters,
 									  languageOid,
-									  false,	/* not an aggregate */
+									  stmt->is_procedure ? OBJECT_PROCEDURE : OBJECT_FUNCTION,
 									  &parameterTypes,
 									  &allParameterTypes,
 									  &parameterModes,
@@ -999,7 +1053,14 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 									  &variadicArgType,
 									  &requiredResultType);
 
-	if (stmt->returnType)
+	if (stmt->is_procedure)
+	{
+		Assert(!stmt->returnType);
+
+		prorettype = InvalidOid;
+		returnsSet = false;
+	}
+	else if (stmt->returnType)
 	{
 		/* explicit RETURNS clause */
 		compute_return_type(stmt->returnType, languageOid,
@@ -1045,7 +1106,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 		trftypes = NULL;
 	}
 
-	compute_attributes_with_style(pstate, stmt->withClause, &isStrict, &volatility);
+	compute_attributes_with_style(pstate, stmt->is_procedure, stmt->withClause, &isStrict, &volatility);
 
 	interpret_AS_clause(languageOid, language, funcname, as_clause,
 						&prosrc_str, &probin_str);
@@ -1168,6 +1229,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 	HeapTuple	tup;
 	Oid			funcOid;
 	Form_pg_proc procForm;
+	bool		is_procedure;
 	Relation	rel;
 	ListCell   *l;
 	DefElem    *volatility_item = NULL;
@@ -1182,7 +1244,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 
 	rel = heap_open(ProcedureRelationId, RowExclusiveLock);
 
-	funcOid = LookupFuncWithArgs(stmt->func, false);
+	funcOid = LookupFuncWithArgs(stmt->objtype, stmt->func, false);
 
 	tup = SearchSysCacheCopy1(PROCOID, ObjectIdGetDatum(funcOid));
 	if (!HeapTupleIsValid(tup)) /* should not happen */
@@ -1201,12 +1263,15 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 				 errmsg("\"%s\" is an aggregate function",
 						NameListToString(stmt->func->objname))));
 
+	is_procedure = (procForm->prorettype == InvalidOid);
+
 	/* Examine requested actions. */
 	foreach(l, stmt->actions)
 	{
 		DefElem    *defel = (DefElem *) lfirst(l);
 
 		if (compute_common_attribute(pstate,
+									 is_procedure,
 									 defel,
 									 &volatility_item,
 									 &strict_item,
@@ -1472,7 +1537,7 @@ CreateCast(CreateCastStmt *stmt)
 	{
 		Form_pg_proc procstruct;
 
-		funcid = LookupFuncWithArgs(stmt->func, false);
+		funcid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->func, false);
 
 		tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
 		if (!HeapTupleIsValid(tuple))
@@ -1853,7 +1918,7 @@ CreateTransform(CreateTransformStmt *stmt)
 	 */
 	if (stmt->fromsql)
 	{
-		fromsqlfuncid = LookupFuncWithArgs(stmt->fromsql, false);
+		fromsqlfuncid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->fromsql, false);
 
 		if (!pg_proc_ownercheck(fromsqlfuncid, GetUserId()))
 			aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_PROC, NameListToString(stmt->fromsql->objname));
@@ -1879,7 +1944,7 @@ CreateTransform(CreateTransformStmt *stmt)
 
 	if (stmt->tosql)
 	{
-		tosqlfuncid = LookupFuncWithArgs(stmt->tosql, false);
+		tosqlfuncid = LookupFuncWithArgs(OBJECT_FUNCTION, stmt->tosql, false);
 
 		if (!pg_proc_ownercheck(tosqlfuncid, GetUserId()))
 			aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_PROC, NameListToString(stmt->tosql->objname));
@@ -2168,3 +2233,80 @@ ExecuteDoStmt(DoStmt *stmt)
 	/* execute the inline handler */
 	OidFunctionCall1(laninline, PointerGetDatum(codeblock));
 }
+
+/*
+ * Execute CALL statement
+ */
+void
+ExecuteCallStmt(ParseState *pstate, CallStmt *stmt)
+{
+	List	   *targs;
+	ListCell   *lc;
+	Node	   *node;
+	FuncExpr   *fexpr;
+	int			nargs;
+	int			i;
+	AclResult   aclresult;
+	FmgrInfo	flinfo;
+	FunctionCallInfoData fcinfo;
+
+	targs = NIL;
+	foreach(lc, stmt->funccall->args)
+	{
+		targs = lappend(targs, transformExpr(pstate,
+											 (Node *) lfirst(lc),
+											 EXPR_KIND_CALL));
+	}
+
+	node = ParseFuncOrColumn(pstate,
+							 stmt->funccall->funcname,
+							 targs,
+							 pstate->p_last_srf,
+							 stmt->funccall,
+							 true,
+							 stmt->funccall->location);
+
+	fexpr = castNode(FuncExpr, node);
+
+	aclresult = pg_proc_aclcheck(fexpr->funcid, GetUserId(), ACL_EXECUTE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, ACL_KIND_PROC, get_func_name(fexpr->funcid));
+	InvokeFunctionExecuteHook(fexpr->funcid);
+
+	nargs = list_length(fexpr->args);
+
+	/* safety check; see ExecInitFunc() */
+	if (nargs > FUNC_MAX_ARGS)
+		ereport(ERROR,
+				(errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+				 errmsg_plural("cannot pass more than %d argument to a procedure",
+							   "cannot pass more than %d arguments to a procedure",
+							   FUNC_MAX_ARGS,
+							   FUNC_MAX_ARGS)));
+
+	fmgr_info(fexpr->funcid, &flinfo);
+	InitFunctionCallInfoData(fcinfo, &flinfo, nargs, fexpr->inputcollid, NULL, NULL);
+
+	i = 0;
+	foreach (lc, fexpr->args)
+	{
+		EState	   *estate;
+		ExprState  *exprstate;
+		ExprContext *econtext;
+		Datum		val;
+		bool		isnull;
+
+		estate = CreateExecutorState();
+		exprstate = ExecPrepareExpr(lfirst(lc), estate);
+		econtext = CreateStandaloneExprContext();
+		val = ExecEvalExprSwitchContext(exprstate, econtext, &isnull);
+		FreeExecutorState(estate);
+
+		fcinfo.arg[i] = val;
+		fcinfo.argnull[i] = isnull;
+
+		i++;
+	}
+
+	FunctionCallInvoke(&fcinfo);
+}
diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c
index 1641e68abe..35c7c67bf5 100644
--- a/src/backend/commands/opclasscmds.c
+++ b/src/backend/commands/opclasscmds.c
@@ -520,7 +520,7 @@ DefineOpClass(CreateOpClassStmt *stmt)
 							 errmsg("invalid procedure number %d,"
 									" must be between 1 and %d",
 									item->number, maxProcNumber)));
-				funcOid = LookupFuncWithArgs(item->name, false);
+				funcOid = LookupFuncWithArgs(OBJECT_FUNCTION, item->name, false);
 #ifdef NOT_USED
 				/* XXX this is unnecessary given the superuser check above */
 				/* Caller must own function */
@@ -894,7 +894,7 @@ AlterOpFamilyAdd(AlterOpFamilyStmt *stmt, Oid amoid, Oid opfamilyoid,
 							 errmsg("invalid procedure number %d,"
 									" must be between 1 and %d",
 									item->number, maxProcNumber)));
-				funcOid = LookupFuncWithArgs(item->name, false);
+				funcOid = LookupFuncWithArgs(OBJECT_FUNCTION, item->name, false);
 #ifdef NOT_USED
 				/* XXX this is unnecessary given the superuser check above */
 				/* Caller must own function */
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 98eb777421..3caa343723 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -390,6 +390,7 @@ sql_fn_post_column_ref(ParseState *pstate, ColumnRef *cref, Node *var)
 								  list_make1(param),
 								  pstate->p_last_srf,
 								  NULL,
+								  false,
 								  cref->location);
 	}
 
@@ -658,7 +659,8 @@ init_sql_fcache(FmgrInfo *finfo, Oid collation, bool lazyEvalOK)
 	fcache->rettype = rettype;
 
 	/* Fetch the typlen and byval info for the result type */
-	get_typlenbyval(rettype, &fcache->typlen, &fcache->typbyval);
+	if (rettype)
+		get_typlenbyval(rettype, &fcache->typlen, &fcache->typbyval);
 
 	/* Remember whether we're returning setof something */
 	fcache->returnsSet = procedureStruct->proretset;
@@ -1321,8 +1323,8 @@ fmgr_sql(PG_FUNCTION_ARGS)
 		}
 		else
 		{
-			/* Should only get here for VOID functions */
-			Assert(fcache->rettype == VOIDOID);
+			/* Should only get here for procedures and VOID functions */
+			Assert(fcache->rettype == InvalidOid || fcache->rettype == VOIDOID);
 			fcinfo->isnull = true;
 			result = (Datum) 0;
 		}
@@ -1546,7 +1548,10 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
 	if (modifyTargetList)
 		*modifyTargetList = false;	/* initialize for no change */
 	if (junkFilter)
-		*junkFilter = NULL;		/* initialize in case of VOID result */
+		*junkFilter = NULL;		/* initialize in case of procedure/VOID result */
+
+	if (!rettype)
+		return false;
 
 	/*
 	 * Find the last canSetTag query in the list.  This isn't necessarily the
@@ -1591,7 +1596,7 @@ check_sql_fn_retval(Oid func_id, Oid rettype, List *queryTreeList,
 	else
 	{
 		/* Empty function body, or last statement is a utility command */
-		if (rettype != VOIDOID)
+		if (rettype && rettype != VOIDOID)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 					 errmsg("return type mismatch in function declared to return %s",
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index d9ff8a7e51..aff9a62106 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3210,6 +3210,16 @@ _copyClosePortalStmt(const ClosePortalStmt *from)
 	return newnode;
 }
 
+static CallStmt *
+_copyCallStmt(const CallStmt *from)
+{
+	CallStmt *newnode = makeNode(CallStmt);
+
+	COPY_NODE_FIELD(funccall);
+
+	return newnode;
+}
+
 static ClusterStmt *
 _copyClusterStmt(const ClusterStmt *from)
 {
@@ -3411,6 +3421,7 @@ _copyCreateFunctionStmt(const CreateFunctionStmt *from)
 	COPY_NODE_FIELD(funcname);
 	COPY_NODE_FIELD(parameters);
 	COPY_NODE_FIELD(returnType);
+	COPY_SCALAR_FIELD(is_procedure);
 	COPY_NODE_FIELD(options);
 	COPY_NODE_FIELD(withClause);
 
@@ -3435,6 +3446,7 @@ _copyAlterFunctionStmt(const AlterFunctionStmt *from)
 {
 	AlterFunctionStmt *newnode = makeNode(AlterFunctionStmt);
 
+	COPY_SCALAR_FIELD(objtype);
 	COPY_NODE_FIELD(func);
 	COPY_NODE_FIELD(actions);
 
@@ -5104,6 +5116,9 @@ copyObjectImpl(const void *from)
 		case T_ClosePortalStmt:
 			retval = _copyClosePortalStmt(from);
 			break;
+		case T_CallStmt:
+			retval = _copyCallStmt(from);
+			break;
 		case T_ClusterStmt:
 			retval = _copyClusterStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 2866fd7b4a..2e869a9d5d 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1201,6 +1201,14 @@ _equalClosePortalStmt(const ClosePortalStmt *a, const ClosePortalStmt *b)
 	return true;
 }
 
+static bool
+_equalCallStmt(const CallStmt *a, const CallStmt *b)
+{
+	COMPARE_NODE_FIELD(funccall);
+
+	return true;
+}
+
 static bool
 _equalClusterStmt(const ClusterStmt *a, const ClusterStmt *b)
 {
@@ -1364,6 +1372,7 @@ _equalCreateFunctionStmt(const CreateFunctionStmt *a, const CreateFunctionStmt *
 	COMPARE_NODE_FIELD(funcname);
 	COMPARE_NODE_FIELD(parameters);
 	COMPARE_NODE_FIELD(returnType);
+	COMPARE_SCALAR_FIELD(is_procedure);
 	COMPARE_NODE_FIELD(options);
 	COMPARE_NODE_FIELD(withClause);
 
@@ -1384,6 +1393,7 @@ _equalFunctionParameter(const FunctionParameter *a, const FunctionParameter *b)
 static bool
 _equalAlterFunctionStmt(const AlterFunctionStmt *a, const AlterFunctionStmt *b)
 {
+	COMPARE_SCALAR_FIELD(objtype);
 	COMPARE_NODE_FIELD(func);
 	COMPARE_NODE_FIELD(actions);
 
@@ -3246,6 +3256,9 @@ equal(const void *a, const void *b)
 		case T_ClosePortalStmt:
 			retval = _equalClosePortalStmt(a, b);
 			break;
+		case T_CallStmt:
+			retval = _equalCallStmt(a, b);
+			break;
 		case T_ClusterStmt:
 			retval = _equalClusterStmt(a, b);
 			break;
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index d14ef31eae..419c3ccd91 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4401,6 +4401,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 	if (funcform->prolang != SQLlanguageId ||
 		funcform->prosecdef ||
 		funcform->proretset ||
+		funcform->prorettype == InvalidOid ||
 		funcform->prorettype == RECORDOID ||
 		!heap_attisnull(func_tuple, Anum_pg_proc_proconfig) ||
 		funcform->pronargs != list_length(args))
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c301ca465d..ebfc94f896 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -253,7 +253,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		AlterCompositeTypeStmt AlterUserMappingStmt
 		AlterRoleStmt AlterRoleSetStmt AlterPolicyStmt
 		AlterDefaultPrivilegesStmt DefACLAction
-		AnalyzeStmt ClosePortalStmt ClusterStmt CommentStmt
+		AnalyzeStmt CallStmt ClosePortalStmt ClusterStmt CommentStmt
 		ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt
 		CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
@@ -611,7 +611,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	BACKWARD BEFORE BEGIN_P BETWEEN BIGINT BINARY BIT
 	BOOLEAN_P BOTH BY
 
-	CACHE CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
+	CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
 	CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
 	CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
 	COMMITTED CONCURRENTLY CONFIGURATION CONFLICT CONNECTION CONSTRAINT
@@ -660,14 +660,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
-	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROGRAM PUBLICATION
+	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
 	QUOTE
 
 	RANGE READ REAL REASSIGN RECHECK RECURSIVE REF REFERENCES REFERENCING
 	REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA
 	RESET RESTART RESTRICT RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP
-	ROW ROWS RULE
+	ROUTINE ROUTINES ROW ROWS RULE
 
 	SAVEPOINT SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
 	SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
@@ -845,6 +845,7 @@ stmt :
 			| AlterTSDictionaryStmt
 			| AlterUserMappingStmt
 			| AnalyzeStmt
+			| CallStmt
 			| CheckPointStmt
 			| ClosePortalStmt
 			| ClusterStmt
@@ -940,6 +941,20 @@ stmt :
 				{ $$ = NULL; }
 		;
 
+/*****************************************************************************
+ *
+ * CALL statement
+ *
+ *****************************************************************************/
+
+CallStmt:	CALL func_application
+				{
+					CallStmt *n = makeNode(CallStmt);
+					n->funccall = castNode(FuncCall, $2);
+					$$ = (Node *)n;
+				}
+		;
+
 /*****************************************************************************
  *
  * Create a new Postgres DBMS role
@@ -4554,6 +4569,24 @@ AlterExtensionContentsStmt:
 					n->object = (Node *) lcons(makeString($9), $7);
 					$$ = (Node *)n;
 				}
+			| ALTER EXTENSION name add_drop PROCEDURE function_with_argtypes
+				{
+					AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
+					n->extname = $3;
+					n->action = $4;
+					n->objtype = OBJECT_PROCEDURE;
+					n->object = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			| ALTER EXTENSION name add_drop ROUTINE function_with_argtypes
+				{
+					AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
+					n->extname = $3;
+					n->action = $4;
+					n->objtype = OBJECT_ROUTINE;
+					n->object = (Node *) $6;
+					$$ = (Node *)n;
+				}
 			| ALTER EXTENSION name add_drop SCHEMA name
 				{
 					AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
@@ -6436,6 +6469,22 @@ CommentStmt:
 					n->comment = $8;
 					$$ = (Node *) n;
 				}
+			| COMMENT ON PROCEDURE function_with_argtypes IS comment_text
+				{
+					CommentStmt *n = makeNode(CommentStmt);
+					n->objtype = OBJECT_PROCEDURE;
+					n->object = (Node *) $4;
+					n->comment = $6;
+					$$ = (Node *) n;
+				}
+			| COMMENT ON ROUTINE function_with_argtypes IS comment_text
+				{
+					CommentStmt *n = makeNode(CommentStmt);
+					n->objtype = OBJECT_ROUTINE;
+					n->object = (Node *) $4;
+					n->comment = $6;
+					$$ = (Node *) n;
+				}
 			| COMMENT ON RULE name ON any_name IS comment_text
 				{
 					CommentStmt *n = makeNode(CommentStmt);
@@ -6614,6 +6663,26 @@ SecLabelStmt:
 					n->label = $9;
 					$$ = (Node *) n;
 				}
+			| SECURITY LABEL opt_provider ON PROCEDURE function_with_argtypes
+			  IS security_label
+				{
+					SecLabelStmt *n = makeNode(SecLabelStmt);
+					n->provider = $3;
+					n->objtype = OBJECT_PROCEDURE;
+					n->object = (Node *) $6;
+					n->label = $8;
+					$$ = (Node *) n;
+				}
+			| SECURITY LABEL opt_provider ON ROUTINE function_with_argtypes
+			  IS security_label
+				{
+					SecLabelStmt *n = makeNode(SecLabelStmt);
+					n->provider = $3;
+					n->objtype = OBJECT_ROUTINE;
+					n->object = (Node *) $6;
+					n->label = $8;
+					$$ = (Node *) n;
+				}
 		;
 
 opt_provider:	FOR NonReservedWord_or_Sconst	{ $$ = $2; }
@@ -6977,6 +7046,22 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| PROCEDURE function_with_argtypes_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = ACL_OBJECT_PROCEDURE;
+					n->objs = $2;
+					$$ = n;
+				}
+			| ROUTINE function_with_argtypes_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = ACL_OBJECT_ROUTINE;
+					n->objs = $2;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -7057,6 +7142,22 @@ privilege_target:
 					n->objs = $5;
 					$$ = n;
 				}
+			| ALL PROCEDURES IN_P SCHEMA name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_ALL_IN_SCHEMA;
+					n->objtype = ACL_OBJECT_PROCEDURE;
+					n->objs = $5;
+					$$ = n;
+				}
+			| ALL ROUTINES IN_P SCHEMA name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_ALL_IN_SCHEMA;
+					n->objtype = ACL_OBJECT_ROUTINE;
+					n->objs = $5;
+					$$ = n;
+				}
 		;
 
 
@@ -7213,6 +7314,7 @@ DefACLAction:
 defacl_privilege_target:
 			TABLES			{ $$ = ACL_OBJECT_RELATION; }
 			| FUNCTIONS		{ $$ = ACL_OBJECT_FUNCTION; }
+			| ROUTINES		{ $$ = ACL_OBJECT_FUNCTION; }
 			| SEQUENCES		{ $$ = ACL_OBJECT_SEQUENCE; }
 			| TYPES_P		{ $$ = ACL_OBJECT_TYPE; }
 			| SCHEMAS		{ $$ = ACL_OBJECT_NAMESPACE; }
@@ -7413,6 +7515,18 @@ CreateFunctionStmt:
 					n->withClause = $7;
 					$$ = (Node *)n;
 				}
+			| CREATE opt_or_replace PROCEDURE func_name func_args_with_defaults
+			  createfunc_opt_list
+				{
+					CreateFunctionStmt *n = makeNode(CreateFunctionStmt);
+					n->replace = $2;
+					n->funcname = $4;
+					n->parameters = $5;
+					n->returnType = NULL;
+					n->is_procedure = true;
+					n->options = $6;
+					$$ = (Node *)n;
+				}
 		;
 
 opt_or_replace:
@@ -7830,7 +7944,7 @@ table_func_column_list:
 		;
 
 /*****************************************************************************
- * ALTER FUNCTION
+ * ALTER FUNCTION / ALTER PROCEDURE / ALTER ROUTINE
  *
  * RENAME and OWNER subcommands are already provided by the generic
  * ALTER infrastructure, here we just specify alterations that can
@@ -7841,6 +7955,23 @@ AlterFunctionStmt:
 			ALTER FUNCTION function_with_argtypes alterfunc_opt_list opt_restrict
 				{
 					AlterFunctionStmt *n = makeNode(AlterFunctionStmt);
+					n->objtype = OBJECT_FUNCTION;
+					n->func = $3;
+					n->actions = $4;
+					$$ = (Node *) n;
+				}
+			| ALTER PROCEDURE function_with_argtypes alterfunc_opt_list opt_restrict
+				{
+					AlterFunctionStmt *n = makeNode(AlterFunctionStmt);
+					n->objtype = OBJECT_PROCEDURE;
+					n->func = $3;
+					n->actions = $4;
+					$$ = (Node *) n;
+				}
+			| ALTER ROUTINE function_with_argtypes alterfunc_opt_list opt_restrict
+				{
+					AlterFunctionStmt *n = makeNode(AlterFunctionStmt);
+					n->objtype = OBJECT_ROUTINE;
 					n->func = $3;
 					n->actions = $4;
 					$$ = (Node *) n;
@@ -7865,6 +7996,8 @@ opt_restrict:
  *		QUERY:
  *
  *		DROP FUNCTION funcname (arg1, arg2, ...) [ RESTRICT | CASCADE ]
+ *		DROP PROCEDURE procname (arg1, arg2, ...) [ RESTRICT | CASCADE ]
+ *		DROP ROUTINE routname (arg1, arg2, ...) [ RESTRICT | CASCADE ]
  *		DROP AGGREGATE aggname (arg1, ...) [ RESTRICT | CASCADE ]
  *		DROP OPERATOR opname (leftoperand_typ, rightoperand_typ) [ RESTRICT | CASCADE ]
  *
@@ -7891,6 +8024,46 @@ RemoveFuncStmt:
 					n->concurrent = false;
 					$$ = (Node *)n;
 				}
+			| DROP PROCEDURE function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_PROCEDURE;
+					n->objects = $3;
+					n->behavior = $4;
+					n->missing_ok = false;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
+			| DROP PROCEDURE IF_P EXISTS function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_PROCEDURE;
+					n->objects = $5;
+					n->behavior = $6;
+					n->missing_ok = true;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
+			| DROP ROUTINE function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_ROUTINE;
+					n->objects = $3;
+					n->behavior = $4;
+					n->missing_ok = false;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
+			| DROP ROUTINE IF_P EXISTS function_with_argtypes_list opt_drop_behavior
+				{
+					DropStmt *n = makeNode(DropStmt);
+					n->removeType = OBJECT_ROUTINE;
+					n->objects = $5;
+					n->behavior = $6;
+					n->missing_ok = true;
+					n->concurrent = false;
+					$$ = (Node *)n;
+				}
 		;
 
 RemoveAggrStmt:
@@ -8348,6 +8521,15 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = true;
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -8357,6 +8539,15 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ROUTINE function_with_argtypes RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER SCHEMA name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -8736,6 +8927,22 @@ AlterObjectDependsStmt:
 					n->extname = makeString($7);
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes DEPENDS ON EXTENSION name
+				{
+					AlterObjectDependsStmt *n = makeNode(AlterObjectDependsStmt);
+					n->objectType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->extname = makeString($7);
+					$$ = (Node *)n;
+				}
+			| ALTER ROUTINE function_with_argtypes DEPENDS ON EXTENSION name
+				{
+					AlterObjectDependsStmt *n = makeNode(AlterObjectDependsStmt);
+					n->objectType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->extname = makeString($7);
+					$$ = (Node *)n;
+				}
 			| ALTER TRIGGER name ON qualified_name DEPENDS ON EXTENSION name
 				{
 					AlterObjectDependsStmt *n = makeNode(AlterObjectDependsStmt);
@@ -8851,6 +9058,24 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER ROUTINE function_with_argtypes SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER TABLE relation_expr SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -9126,6 +9351,22 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $9;
 					$$ = (Node *)n;
 				}
+			| ALTER PROCEDURE function_with_argtypes OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_PROCEDURE;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
+			| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_ROUTINE;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
 			| ALTER SCHEMA name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -14689,6 +14930,7 @@ unreserved_keyword:
 			| BEGIN_P
 			| BY
 			| CACHE
+			| CALL
 			| CALLED
 			| CASCADE
 			| CASCADED
@@ -14848,6 +15090,7 @@ unreserved_keyword:
 			| PRIVILEGES
 			| PROCEDURAL
 			| PROCEDURE
+			| PROCEDURES
 			| PROGRAM
 			| PUBLICATION
 			| QUOTE
@@ -14874,6 +15117,8 @@ unreserved_keyword:
 			| ROLE
 			| ROLLBACK
 			| ROLLUP
+			| ROUTINE
+			| ROUTINES
 			| ROWS
 			| RULE
 			| SAVEPOINT
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 64111f315e..4c4f4cdc3d 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -508,6 +508,14 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 
 			break;
 
+		case EXPR_KIND_CALL:
+			if (isAgg)
+				err = _("aggregate functions are not allowed in CALL arguments");
+			else
+				err = _("grouping operations are not allowed in CALL arguments");
+
+			break;
+
 			/*
 			 * There is intentionally no default: case here, so that the
 			 * compiler will warn if we add a new ParseExprKind without
@@ -883,6 +891,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_PARTITION_EXPRESSION:
 			err = _("window functions are not allowed in partition key expression");
 			break;
+		case EXPR_KIND_CALL:
+			err = _("window functions are not allowed in CALL arguments");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 86d1da0677..29f9da796f 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -480,6 +480,7 @@ transformIndirection(ParseState *pstate, A_Indirection *ind)
 										  list_make1(result),
 										  last_srf,
 										  NULL,
+										  false,
 										  location);
 			if (newresult == NULL)
 				unknown_attribute(pstate, result, strVal(n), location);
@@ -629,6 +630,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 											 list_make1(node),
 											 pstate->p_last_srf,
 											 NULL,
+											 false,
 											 cref->location);
 				}
 				break;
@@ -676,6 +678,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 											 list_make1(node),
 											 pstate->p_last_srf,
 											 NULL,
+											 false,
 											 cref->location);
 				}
 				break;
@@ -736,6 +739,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 											 list_make1(node),
 											 pstate->p_last_srf,
 											 NULL,
+											 false,
 											 cref->location);
 				}
 				break;
@@ -1477,6 +1481,7 @@ transformFuncCall(ParseState *pstate, FuncCall *fn)
 							 targs,
 							 last_srf,
 							 fn,
+							 false,
 							 fn->location);
 }
 
@@ -1812,6 +1817,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
+		case EXPR_KIND_CALL:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -3462,6 +3468,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "WHEN";
 		case EXPR_KIND_PARTITION_EXPRESSION:
 			return "PARTITION BY";
+		case EXPR_KIND_CALL:
+			return "CALL";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index a11843332b..2f20516e76 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -71,7 +71,7 @@ static Node *ParseComplexProjection(ParseState *pstate, const char *funcname,
  */
 Node *
 ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
-				  Node *last_srf, FuncCall *fn, int location)
+				  Node *last_srf, FuncCall *fn, bool proc_call, int location)
 {
 	bool		is_column = (fn == NULL);
 	List	   *agg_order = (fn ? fn->agg_order : NIL);
@@ -263,7 +263,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 						   actual_arg_types[0], rettype, -1,
 						   COERCION_EXPLICIT, COERCE_EXPLICIT_CALL, location);
 	}
-	else if (fdresult == FUNCDETAIL_NORMAL)
+	else if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
 	{
 		/*
 		 * Normal function found; was there anything indicating it must be an
@@ -306,6 +306,26 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 					 errmsg("OVER specified, but %s is not a window function nor an aggregate function",
 							NameListToString(funcname)),
 					 parser_errposition(pstate, location)));
+
+		if (fdresult == FUNCDETAIL_NORMAL && proc_call)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_FUNCTION),
+					 errmsg("%s is not a procedure",
+							func_signature_string(funcname, nargs,
+												  argnames,
+												  actual_arg_types)),
+					 errhint("To call a function, use SELECT."),
+					 parser_errposition(pstate, location)));
+
+		if (fdresult == FUNCDETAIL_PROCEDURE && !proc_call)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_FUNCTION),
+					 errmsg("%s is a procedure",
+							func_signature_string(funcname, nargs,
+												  argnames,
+												  actual_arg_types)),
+					 errhint("To call a procedure, use CALL."),
+					 parser_errposition(pstate, location)));
 	}
 	else if (fdresult == FUNCDETAIL_AGGREGATE)
 	{
@@ -635,7 +655,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 		check_srf_call_placement(pstate, last_srf, location);
 
 	/* build the appropriate output structure */
-	if (fdresult == FUNCDETAIL_NORMAL)
+	if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
 	{
 		FuncExpr   *funcexpr = makeNode(FuncExpr);
 
@@ -1589,6 +1609,8 @@ func_get_detail(List *funcname,
 			result = FUNCDETAIL_AGGREGATE;
 		else if (pform->proiswindow)
 			result = FUNCDETAIL_WINDOWFUNC;
+		else if (pform->prorettype == InvalidOid)
+			result = FUNCDETAIL_PROCEDURE;
 		else
 			result = FUNCDETAIL_NORMAL;
 		ReleaseSysCache(ftup);
@@ -1984,16 +2006,28 @@ LookupFuncName(List *funcname, int nargs, const Oid *argtypes, bool noError)
 
 /*
  * LookupFuncWithArgs
- *		Like LookupFuncName, but the argument types are specified by a
- *		ObjectWithArgs node.
+ *
+ * Like LookupFuncName, but the argument types are specified by a
+ * ObjectWithArgs node.  Also, this function can check whether the result is a
+ * function, procedure, or aggregate, based on the objtype argument.  Pass
+ * OBJECT_ROUTINE to accept any of them.
+ *
+ * For historical reasons, we also accept aggregates when looking for a
+ * function.
  */
 Oid
-LookupFuncWithArgs(ObjectWithArgs *func, bool noError)
+LookupFuncWithArgs(ObjectType objtype, ObjectWithArgs *func, bool noError)
 {
 	Oid			argoids[FUNC_MAX_ARGS];
 	int			argcount;
 	int			i;
 	ListCell   *args_item;
+	Oid			oid;
+
+	Assert(objtype == OBJECT_AGGREGATE ||
+		   objtype == OBJECT_FUNCTION ||
+		   objtype == OBJECT_PROCEDURE ||
+		   objtype == OBJECT_ROUTINE);
 
 	argcount = list_length(func->objargs);
 	if (argcount > FUNC_MAX_ARGS)
@@ -2013,90 +2047,100 @@ LookupFuncWithArgs(ObjectWithArgs *func, bool noError)
 		args_item = lnext(args_item);
 	}
 
-	return LookupFuncName(func->objname, func->args_unspecified ? -1 : argcount, argoids, noError);
-}
-
-/*
- * LookupAggWithArgs
- *		Find an aggregate function from a given ObjectWithArgs node.
- *
- * This is almost like LookupFuncWithArgs, but the error messages refer
- * to aggregates rather than plain functions, and we verify that the found
- * function really is an aggregate.
- */
-Oid
-LookupAggWithArgs(ObjectWithArgs *agg, bool noError)
-{
-	Oid			argoids[FUNC_MAX_ARGS];
-	int			argcount;
-	int			i;
-	ListCell   *lc;
-	Oid			oid;
-	HeapTuple	ftup;
-	Form_pg_proc pform;
-
-	argcount = list_length(agg->objargs);
-	if (argcount > FUNC_MAX_ARGS)
-		ereport(ERROR,
-				(errcode(ERRCODE_TOO_MANY_ARGUMENTS),
-				 errmsg_plural("functions cannot have more than %d argument",
-							   "functions cannot have more than %d arguments",
-							   FUNC_MAX_ARGS,
-							   FUNC_MAX_ARGS)));
+	/*
+	 * When looking for a function or routine, we pass noError through to
+	 * LookupFuncName and let it make any error messages.  Otherwise, we make
+	 * our own errors for the aggregate and procedure cases.
+	 */
+	oid = LookupFuncName(func->objname, func->args_unspecified ? -1 : argcount, argoids,
+						 (objtype == OBJECT_FUNCTION || objtype == OBJECT_ROUTINE) ? noError : true);
 
-	i = 0;
-	foreach(lc, agg->objargs)
+	if (objtype == OBJECT_FUNCTION)
 	{
-		TypeName   *t = (TypeName *) lfirst(lc);
-
-		argoids[i] = LookupTypeNameOid(NULL, t, noError);
-		i++;
+		/* Make sure it's a function, not a procedure */
+		if (oid && get_func_rettype(oid) == InvalidOid)
+		{
+			if (noError)
+				return InvalidOid;
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("%s is not a function",
+							func_signature_string(func->objname, argcount,
+												  NIL, argoids))));
+		}
 	}
-
-	oid = LookupFuncName(agg->objname, argcount, argoids, true);
-
-	if (!OidIsValid(oid))
+	else if (objtype == OBJECT_PROCEDURE)
 	{
-		if (noError)
-			return InvalidOid;
-		if (argcount == 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_UNDEFINED_FUNCTION),
-					 errmsg("aggregate %s(*) does not exist",
-							NameListToString(agg->objname))));
-		else
+		if (!OidIsValid(oid))
+		{
+			if (noError)
+				return InvalidOid;
+			else if (func->args_unspecified)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("could not find a procedure named \"%s\"",
+								NameListToString(func->objname))));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("procedure %s does not exist",
+								func_signature_string(func->objname, argcount,
+													  NIL, argoids))));
+		}
+
+		/* Make sure it's a procedure */
+		if (get_func_rettype(oid) != InvalidOid)
+		{
+			if (noError)
+				return InvalidOid;
 			ereport(ERROR,
-					(errcode(ERRCODE_UNDEFINED_FUNCTION),
-					 errmsg("aggregate %s does not exist",
-							func_signature_string(agg->objname, argcount,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("%s is not a procedure",
+							func_signature_string(func->objname, argcount,
 												  NIL, argoids))));
+		}
 	}
-
-	/* Make sure it's an aggregate */
-	ftup = SearchSysCache1(PROCOID, ObjectIdGetDatum(oid));
-	if (!HeapTupleIsValid(ftup))	/* should not happen */
-		elog(ERROR, "cache lookup failed for function %u", oid);
-	pform = (Form_pg_proc) GETSTRUCT(ftup);
-
-	if (!pform->proisagg)
+	else if (objtype == OBJECT_AGGREGATE)
 	{
-		ReleaseSysCache(ftup);
-		if (noError)
-			return InvalidOid;
-		/* we do not use the (*) notation for functions... */
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("function %s is not an aggregate",
-						func_signature_string(agg->objname, argcount,
-											  NIL, argoids))));
-	}
+		if (!OidIsValid(oid))
+		{
+			if (noError)
+				return InvalidOid;
+			else if (func->args_unspecified)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("could not find a aggregate named \"%s\"",
+								NameListToString(func->objname))));
+			else if (argcount == 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("aggregate %s(*) does not exist",
+								NameListToString(func->objname))));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_FUNCTION),
+						 errmsg("aggregate %s does not exist",
+								func_signature_string(func->objname, argcount,
+													  NIL, argoids))));
+		}
 
-	ReleaseSysCache(ftup);
+		/* Make sure it's an aggregate */
+		if (!get_func_isagg(oid))
+		{
+			if (noError)
+				return InvalidOid;
+			/* we do not use the (*) notation for functions... */
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("function %s is not an aggregate",
+							func_signature_string(func->objname, argcount,
+												  NIL, argoids))));
+		}
+	}
 
 	return oid;
 }
 
-
 /*
  * check_srf_call_placement
  *		Verify that a set-returning function is called in a valid place,
@@ -2236,6 +2280,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_PARTITION_EXPRESSION:
 			err = _("set-returning functions are not allowed in partition key expressions");
 			break;
+		case EXPR_KIND_CALL:
+			err = _("set-returning functions are not allowed in CALL arguments");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 82a707af7b..4da1f8f643 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -657,6 +657,10 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			}
 			break;
 
+		case T_CallStmt:
+			ExecuteCallStmt(pstate, castNode(CallStmt, parsetree));
+			break;
+
 		case T_ClusterStmt:
 			/* we choose to allow this during "read only" transactions */
 			PreventCommandDuringRecovery("CLUSTER");
@@ -1957,9 +1961,15 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_POLICY:
 			tag = "ALTER POLICY";
 			break;
+		case OBJECT_PROCEDURE:
+			tag = "ALTER PROCEDURE";
+			break;
 		case OBJECT_ROLE:
 			tag = "ALTER ROLE";
 			break;
+		case OBJECT_ROUTINE:
+			tag = "ALTER ROUTINE";
+			break;
 		case OBJECT_RULE:
 			tag = "ALTER RULE";
 			break;
@@ -2261,6 +2271,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_FUNCTION:
 					tag = "DROP FUNCTION";
 					break;
+				case OBJECT_PROCEDURE:
+					tag = "DROP PROCEDURE";
+					break;
+				case OBJECT_ROUTINE:
+					tag = "DROP ROUTINE";
+					break;
 				case OBJECT_AGGREGATE:
 					tag = "DROP AGGREGATE";
 					break;
@@ -2359,7 +2375,20 @@ CreateCommandTag(Node *parsetree)
 			break;
 
 		case T_AlterFunctionStmt:
-			tag = "ALTER FUNCTION";
+			switch (((AlterFunctionStmt *) parsetree)->objtype)
+			{
+				case OBJECT_FUNCTION:
+					tag = "ALTER FUNCTION";
+					break;
+				case OBJECT_PROCEDURE:
+					tag = "ALTER PROCEDURE";
+					break;
+				case OBJECT_ROUTINE:
+					tag = "ALTER ROUTINE";
+					break;
+				default:
+					tag = "???";
+			}
 			break;
 
 		case T_GrantStmt:
@@ -2438,7 +2467,10 @@ CreateCommandTag(Node *parsetree)
 			break;
 
 		case T_CreateFunctionStmt:
-			tag = "CREATE FUNCTION";
+			if (((CreateFunctionStmt *) parsetree)->is_procedure)
+				tag = "CREATE PROCEDURE";
+			else
+				tag = "CREATE FUNCTION";
 			break;
 
 		case T_IndexStmt:
@@ -2493,6 +2525,10 @@ CreateCommandTag(Node *parsetree)
 			tag = "LOAD";
 			break;
 
+		case T_CallStmt:
+			tag = "CALL";
+			break;
+
 		case T_ClusterStmt:
 			tag = "CLUSTER";
 			break;
@@ -3116,6 +3152,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_ALL;
 			break;
 
+		case T_CallStmt:
+			lev = LOGSTMT_ALL;
+			break;
+
 		case T_ClusterStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 06cf32f5d7..8514c21c40 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2691,6 +2691,12 @@ pg_get_function_result(PG_FUNCTION_ARGS)
 	if (!HeapTupleIsValid(proctup))
 		PG_RETURN_NULL();
 
+	if (((Form_pg_proc) GETSTRUCT(proctup))->prorettype == InvalidOid)
+	{
+		ReleaseSysCache(proctup);
+		PG_RETURN_NULL();
+	}
+
 	initStringInfo(&buf);
 
 	print_function_rettype(&buf, proctup);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 0ea2f2bc54..5211360777 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1614,6 +1614,25 @@ func_parallel(Oid funcid)
 	return result;
 }
 
+/*
+ * get_func_isagg
+ *	   Given procedure id, return the function's proisagg field.
+ */
+bool
+get_func_isagg(Oid funcid)
+{
+	HeapTuple	tp;
+	bool		result;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+
+	result = ((Form_pg_proc) GETSTRUCT(tp))->proisagg;
+	ReleaseSysCache(tp);
+	return result;
+}
+
 /*
  * get_func_leakproof
  *	   Given procedure id, return the function's leakproof field.
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 70d8f24d17..12290a1aae 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -33,7 +33,7 @@ static void AddAcl(PQExpBuffer aclbuf, const char *keyword,
  *	name: the object name, in the form to use in the commands (already quoted)
  *	subname: the sub-object name, if any (already quoted); NULL if none
  *	type: the object type (as seen in GRANT command: must be one of
- *		TABLE, SEQUENCE, FUNCTION, LANGUAGE, SCHEMA, DATABASE, TABLESPACE,
+ *		TABLE, SEQUENCE, FUNCTION, PROCEDURE, LANGUAGE, SCHEMA, DATABASE, TABLESPACE,
  *		FOREIGN DATA WRAPPER, SERVER, or LARGE OBJECT)
  *	acls: the ACL string fetched from the database
  *	racls: the ACL string of any initial-but-now-revoked privileges
@@ -524,6 +524,9 @@ do { \
 	else if (strcmp(type, "FUNCTION") == 0 ||
 			 strcmp(type, "FUNCTIONS") == 0)
 		CONVERT_PRIV('X', "EXECUTE");
+	else if (strcmp(type, "PROCEDURE") == 0 ||
+			 strcmp(type, "PROCEDURES") == 0)
+		CONVERT_PRIV('X', "EXECUTE");
 	else if (strcmp(type, "LANGUAGE") == 0)
 		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "SCHEMA") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index ec2fa8b9b9..41741aefbc 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2889,7 +2889,8 @@ _tocEntryRequired(TocEntry *te, teSection curSection, RestoreOptions *ropt)
 			if (ropt->indexNames.head != NULL && (!(simple_string_list_member(&ropt->indexNames, te->tag))))
 				return 0;
 		}
-		else if (strcmp(te->desc, "FUNCTION") == 0)
+		else if (strcmp(te->desc, "FUNCTION") == 0 ||
+				 strcmp(te->desc, "PROCEDURE") == 0)
 		{
 			if (!ropt->selFunction)
 				return 0;
@@ -3388,7 +3389,8 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te, ArchiveHandle *AH)
 		strcmp(type, "FUNCTION") == 0 ||
 		strcmp(type, "OPERATOR") == 0 ||
 		strcmp(type, "OPERATOR CLASS") == 0 ||
-		strcmp(type, "OPERATOR FAMILY") == 0)
+		strcmp(type, "OPERATOR FAMILY") == 0 ||
+		strcmp(type, "PROCEDURE") == 0)
 	{
 		/* Chop "DROP " off the front and make a modifiable copy */
 		char	   *first = pg_strdup(te->dropStmt + 5);
@@ -3560,6 +3562,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 			strcmp(te->desc, "OPERATOR") == 0 ||
 			strcmp(te->desc, "OPERATOR CLASS") == 0 ||
 			strcmp(te->desc, "OPERATOR FAMILY") == 0 ||
+			strcmp(te->desc, "PROCEDURE") == 0 ||
 			strcmp(te->desc, "PROCEDURAL LANGUAGE") == 0 ||
 			strcmp(te->desc, "SCHEMA") == 0 ||
 			strcmp(te->desc, "EVENT TRIGGER") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d8fb356130..e6701aaa78 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11349,6 +11349,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 	char	   *funcargs;
 	char	   *funciargs;
 	char	   *funcresult;
+	bool		is_procedure;
 	char	   *proallargtypes;
 	char	   *proargmodes;
 	char	   *proargnames;
@@ -11370,6 +11371,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 	char	  **argnames = NULL;
 	char	  **configitems = NULL;
 	int			nconfigitems = 0;
+	const char *keyword;
 	int			i;
 
 	/* Skip if not to be dumped */
@@ -11513,7 +11515,11 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 	{
 		funcargs = PQgetvalue(res, 0, PQfnumber(res, "funcargs"));
 		funciargs = PQgetvalue(res, 0, PQfnumber(res, "funciargs"));
-		funcresult = PQgetvalue(res, 0, PQfnumber(res, "funcresult"));
+		is_procedure = PQgetisnull(res, 0, PQfnumber(res, "funcresult"));
+		if (is_procedure)
+			funcresult = NULL;
+		else
+			funcresult = PQgetvalue(res, 0, PQfnumber(res, "funcresult"));
 		proallargtypes = proargmodes = proargnames = NULL;
 	}
 	else
@@ -11522,6 +11528,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 		proargmodes = PQgetvalue(res, 0, PQfnumber(res, "proargmodes"));
 		proargnames = PQgetvalue(res, 0, PQfnumber(res, "proargnames"));
 		funcargs = funciargs = funcresult = NULL;
+		is_procedure = false;
 	}
 	if (PQfnumber(res, "protrftypes") != -1)
 		protrftypes = PQgetvalue(res, 0, PQfnumber(res, "protrftypes"));
@@ -11653,22 +11660,29 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 
 	funcsig_tag = format_function_signature(fout, finfo, false);
 
+	keyword = is_procedure ? "PROCEDURE" : "FUNCTION";
+
 	/*
 	 * DROP must be fully qualified in case same name appears in pg_catalog
 	 */
-	appendPQExpBuffer(delqry, "DROP FUNCTION %s.%s;\n",
+	appendPQExpBuffer(delqry, "DROP %s %s.%s;\n",
+					  keyword,
 					  fmtId(finfo->dobj.namespace->dobj.name),
 					  funcsig);
 
-	appendPQExpBuffer(q, "CREATE FUNCTION %s ", funcfullsig ? funcfullsig :
+	appendPQExpBuffer(q, "CREATE %s %s",
+					  keyword,
+					  funcfullsig ? funcfullsig :
 					  funcsig);
-	if (funcresult)
-		appendPQExpBuffer(q, "RETURNS %s", funcresult);
+	if (is_procedure)
+		;
+	else if (funcresult)
+		appendPQExpBuffer(q, " RETURNS %s", funcresult);
 	else
 	{
 		rettypename = getFormattedTypeName(fout, finfo->prorettype,
 										   zeroAsOpaque);
-		appendPQExpBuffer(q, "RETURNS %s%s",
+		appendPQExpBuffer(q, " RETURNS %s%s",
 						  (proretset[0] == 't') ? "SETOF " : "",
 						  rettypename);
 		free(rettypename);
@@ -11775,7 +11789,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 
 	appendPQExpBuffer(q, "\n    %s;\n", asPart->data);
 
-	appendPQExpBuffer(labelq, "FUNCTION %s", funcsig);
+	appendPQExpBuffer(labelq, "%s %s", keyword, funcsig);
 
 	if (dopt->binary_upgrade)
 		binary_upgrade_extension_member(q, &finfo->dobj, labelq->data);
@@ -11786,7 +11800,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 					 finfo->dobj.namespace->dobj.name,
 					 NULL,
 					 finfo->rolname, false,
-					 "FUNCTION", SECTION_PRE_DATA,
+					 keyword, SECTION_PRE_DATA,
 					 q->data, delqry->data, NULL,
 					 NULL, 0,
 					 NULL, NULL);
@@ -11803,7 +11817,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 					 finfo->dobj.catId, 0, finfo->dobj.dumpId);
 
 	if (finfo->dobj.dump & DUMP_COMPONENT_ACL)
-		dumpACL(fout, finfo->dobj.catId, finfo->dobj.dumpId, "FUNCTION",
+		dumpACL(fout, finfo->dobj.catId, finfo->dobj.dumpId, keyword,
 				funcsig, NULL, funcsig_tag,
 				finfo->dobj.namespace->dobj.name,
 				finfo->rolname, finfo->proacl, finfo->rproacl,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index fa3b56a426..7cf9bdadb2 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3654,6 +3654,44 @@
 			section_data             => 1,
 			section_post_data        => 1, }, },
 
+	'CREATE PROCEDURE dump_test.ptest1' => {
+		all_runs     => 1,
+		create_order => 41,
+		create_sql   => 'CREATE PROCEDURE dump_test.ptest1(a int)
+					   LANGUAGE SQL AS $$ INSERT INTO dump_test.test_table (col1) VALUES (a) $$;',
+		regexp => qr/^
+			\QCREATE PROCEDURE ptest1(a integer)\E
+			\n\s+\QLANGUAGE sql\E
+			\n\s+AS\ \$\$\Q INSERT INTO dump_test.test_table (col1) VALUES (a) \E\$\$;
+			/xm,
+		like => {
+			binary_upgrade          => 1,
+			clean                   => 1,
+			clean_if_exists         => 1,
+			createdb                => 1,
+			defaults                => 1,
+			exclude_test_table      => 1,
+			exclude_test_table_data => 1,
+			no_blobs                => 1,
+			no_privs                => 1,
+			no_owner                => 1,
+			only_dump_test_schema   => 1,
+			pg_dumpall_dbprivs      => 1,
+			schema_only             => 1,
+			section_pre_data        => 1,
+			test_schema_plus_blobs  => 1,
+			with_oids               => 1, },
+		unlike => {
+			column_inserts           => 1,
+			data_only                => 1,
+			exclude_dump_test_schema => 1,
+			only_dump_test_table     => 1,
+			pg_dumpall_globals       => 1,
+			pg_dumpall_globals_clean => 1,
+			role                     => 1,
+			section_data             => 1,
+			section_post_data        => 1, }, },
+
 	'CREATE TYPE dump_test.int42 populated' => {
 		all_runs     => 1,
 		create_order => 42,
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 99167104d4..dee0311210 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -353,6 +353,7 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 						  " CASE\n"
 						  "  WHEN p.proisagg THEN '%s'\n"
 						  "  WHEN p.proiswindow THEN '%s'\n"
+						  "  WHEN p.prorettype = 0 THEN '%s'\n"
 						  "  WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN '%s'\n"
 						  "  ELSE '%s'\n"
 						  " END as \"%s\"",
@@ -361,8 +362,9 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* translator: "agg" is short for "aggregate" */
 						  gettext_noop("agg"),
 						  gettext_noop("window"),
+						  gettext_noop("proc"),
 						  gettext_noop("trigger"),
-						  gettext_noop("normal"),
+						  gettext_noop("func"),
 						  gettext_noop("Type"));
 	else if (pset.sversion >= 80100)
 		appendPQExpBuffer(&buf,
@@ -407,7 +409,7 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* translator: "agg" is short for "aggregate" */
 						  gettext_noop("agg"),
 						  gettext_noop("trigger"),
-						  gettext_noop("normal"),
+						  gettext_noop("func"),
 						  gettext_noop("Type"));
 	else
 		appendPQExpBuffer(&buf,
@@ -424,7 +426,7 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* translator: "agg" is short for "aggregate" */
 						  gettext_noop("agg"),
 						  gettext_noop("trigger"),
-						  gettext_noop("normal"),
+						  gettext_noop("func"),
 						  gettext_noop("Type"));
 
 	if (verbose)
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b3e3799c13..468e50aa31 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -397,7 +397,7 @@ static const SchemaQuery Query_for_list_of_functions = {
 	/* catname */
 	"pg_catalog.pg_proc p",
 	/* selcondition */
-	NULL,
+	"p.prorettype <> 0",
 	/* viscondition */
 	"pg_catalog.pg_function_is_visible(p.oid)",
 	/* namespace */
@@ -423,6 +423,36 @@ static const SchemaQuery Query_for_list_of_indexes = {
 	NULL
 };
 
+static const SchemaQuery Query_for_list_of_procedures = {
+	/* catname */
+	"pg_catalog.pg_proc p",
+	/* selcondition */
+	"p.prorettype = 0",
+	/* viscondition */
+	"pg_catalog.pg_function_is_visible(p.oid)",
+	/* namespace */
+	"p.pronamespace",
+	/* result */
+	"pg_catalog.quote_ident(p.proname)",
+	/* qualresult */
+	NULL
+};
+
+static const SchemaQuery Query_for_list_of_routines = {
+	/* catname */
+	"pg_catalog.pg_proc p",
+	/* selcondition */
+	NULL,
+	/* viscondition */
+	"pg_catalog.pg_function_is_visible(p.oid)",
+	/* namespace */
+	"p.pronamespace",
+	/* result */
+	"pg_catalog.quote_ident(p.proname)",
+	/* qualresult */
+	NULL
+};
+
 static const SchemaQuery Query_for_list_of_sequences = {
 	/* catname */
 	"pg_catalog.pg_class c",
@@ -1032,8 +1062,10 @@ static const pgsql_thing_t words_after_create[] = {
 	{"OWNED", NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},	/* for DROP OWNED BY ... */
 	{"PARSER", Query_for_list_of_ts_parsers, NULL, THING_NO_SHOW},
 	{"POLICY", NULL, NULL},
+	{"PROCEDURE", NULL, &Query_for_list_of_procedures},
 	{"PUBLICATION", Query_for_list_of_publications},
 	{"ROLE", Query_for_list_of_roles},
+	{"ROUTINE", NULL, &Query_for_list_of_routines, THING_NO_CREATE},
 	{"RULE", "SELECT pg_catalog.quote_ident(rulename) FROM pg_catalog.pg_rules WHERE substring(pg_catalog.quote_ident(rulename),1,%d)='%s'"},
 	{"SCHEMA", Query_for_list_of_schemas},
 	{"SEQUENCE", NULL, &Query_for_list_of_sequences},
@@ -1407,7 +1439,7 @@ psql_completion(const char *text, int start, int end)
 
 	/* Known command-starting keywords. */
 	static const char *const sql_commands[] = {
-		"ABORT", "ALTER", "ANALYZE", "BEGIN", "CHECKPOINT", "CLOSE", "CLUSTER",
+		"ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER",
 		"COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE",
 		"DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN",
 		"FETCH", "GRANT", "IMPORT", "INSERT", "LISTEN", "LOAD", "LOCK",
@@ -1520,11 +1552,11 @@ psql_completion(const char *text, int start, int end)
 	/* ALTER TABLE,INDEX,MATERIALIZED VIEW ALL IN TABLESPACE xxx OWNED BY xxx */
 	else if (TailMatches7("ALL", "IN", "TABLESPACE", MatchAny, "OWNED", "BY", MatchAny))
 		COMPLETE_WITH_CONST("SET TABLESPACE");
-	/* ALTER AGGREGATE,FUNCTION <name> */
-	else if (Matches3("ALTER", "AGGREGATE|FUNCTION", MatchAny))
+	/* ALTER AGGREGATE,FUNCTION,PROCEDURE,ROUTINE <name> */
+	else if (Matches3("ALTER", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny))
 		COMPLETE_WITH_CONST("(");
-	/* ALTER AGGREGATE,FUNCTION <name> (...) */
-	else if (Matches4("ALTER", "AGGREGATE|FUNCTION", MatchAny, MatchAny))
+	/* ALTER AGGREGATE,FUNCTION,PROCEDURE,ROUTINE <name> (...) */
+	else if (Matches4("ALTER", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny))
 	{
 		if (ends_with(prev_wd, ')'))
 			COMPLETE_WITH_LIST3("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2145,6 +2177,11 @@ psql_completion(const char *text, int start, int end)
 /* ROLLBACK */
 	else if (Matches1("ROLLBACK"))
 		COMPLETE_WITH_LIST4("WORK", "TRANSACTION", "TO SAVEPOINT", "PREPARED");
+/* CALL */
+	else if (Matches1("CALL"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+	else if (Matches2("CALL", MatchAny))
+		COMPLETE_WITH_CONST("(");
 /* CLUSTER */
 	else if (Matches1("CLUSTER"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, "UNION SELECT 'VERBOSE'");
@@ -2176,6 +2213,7 @@ psql_completion(const char *text, int start, int end)
 			"SERVER", "INDEX", "LANGUAGE", "POLICY", "PUBLICATION", "RULE",
 			"SCHEMA", "SEQUENCE", "STATISTICS", "SUBSCRIPTION",
 			"TABLE", "TYPE", "VIEW", "MATERIALIZED VIEW", "COLUMN", "AGGREGATE", "FUNCTION",
+			"PROCEDURE", "ROUTINE",
 			"OPERATOR", "TRIGGER", "CONSTRAINT", "DOMAIN", "LARGE OBJECT",
 		"TABLESPACE", "TEXT SEARCH", "ROLE", NULL};
 
@@ -2685,7 +2723,7 @@ psql_completion(const char *text, int start, int end)
 					  "COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW",
 					  MatchAny) ||
 			 Matches4("DROP", "ACCESS", "METHOD", MatchAny) ||
-			 (Matches4("DROP", "AGGREGATE|FUNCTION", MatchAny, MatchAny) &&
+			 (Matches4("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
 			 Matches4("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches5("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
@@ -2694,9 +2732,9 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH_LIST2("CASCADE", "RESTRICT");
 
 	/* help completing some of the variants */
-	else if (Matches3("DROP", "AGGREGATE|FUNCTION", MatchAny))
+	else if (Matches3("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny))
 		COMPLETE_WITH_CONST("(");
-	else if (Matches4("DROP", "AGGREGATE|FUNCTION", MatchAny, "("))
+	else if (Matches4("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, "("))
 		COMPLETE_WITH_FUNCTION_ARG(prev2_wd);
 	else if (Matches2("DROP", "FOREIGN"))
 		COMPLETE_WITH_LIST2("DATA WRAPPER", "TABLE");
@@ -2893,10 +2931,12 @@ psql_completion(const char *text, int start, int end)
 		 * objects supported.
 		 */
 		if (HeadMatches3("ALTER", "DEFAULT", "PRIVILEGES"))
-			COMPLETE_WITH_LIST5("TABLES", "SEQUENCES", "FUNCTIONS", "TYPES", "SCHEMAS");
+			COMPLETE_WITH_LIST7("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS");
 		else
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvmf,
 									   " UNION SELECT 'ALL FUNCTIONS IN SCHEMA'"
+									   " UNION SELECT 'ALL PROCEDURES IN SCHEMA'"
+									   " UNION SELECT 'ALL ROUTINES IN SCHEMA'"
 									   " UNION SELECT 'ALL SEQUENCES IN SCHEMA'"
 									   " UNION SELECT 'ALL TABLES IN SCHEMA'"
 									   " UNION SELECT 'DATABASE'"
@@ -2906,6 +2946,8 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'FUNCTION'"
 									   " UNION SELECT 'LANGUAGE'"
 									   " UNION SELECT 'LARGE OBJECT'"
+									   " UNION SELECT 'PROCEDURE'"
+									   " UNION SELECT 'ROUTINE'"
 									   " UNION SELECT 'SCHEMA'"
 									   " UNION SELECT 'SEQUENCE'"
 									   " UNION SELECT 'TABLE'"
@@ -2913,7 +2955,10 @@ psql_completion(const char *text, int start, int end)
 									   " UNION SELECT 'TYPE'");
 	}
 	else if (TailMatches4("GRANT|REVOKE", MatchAny, "ON", "ALL"))
-		COMPLETE_WITH_LIST3("FUNCTIONS IN SCHEMA", "SEQUENCES IN SCHEMA",
+		COMPLETE_WITH_LIST5("FUNCTIONS IN SCHEMA",
+							"PROCEDURES IN SCHEMA",
+							"ROUTINES IN SCHEMA",
+							"SEQUENCES IN SCHEMA",
 							"TABLES IN SCHEMA");
 	else if (TailMatches4("GRANT|REVOKE", MatchAny, "ON", "FOREIGN"))
 		COMPLETE_WITH_LIST2("DATA WRAPPER", "SERVER");
@@ -2934,6 +2979,10 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_functions, NULL);
 		else if (TailMatches1("LANGUAGE"))
 			COMPLETE_WITH_QUERY(Query_for_list_of_languages);
+		else if (TailMatches1("PROCEDURE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_procedures, NULL);
+		else if (TailMatches1("ROUTINE"))
+			COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines, NULL);
 		else if (TailMatches1("SCHEMA"))
 			COMPLETE_WITH_QUERY(Query_for_list_of_schemas);
 		else if (TailMatches1("SEQUENCE"))
@@ -3163,7 +3212,7 @@ psql_completion(const char *text, int start, int end)
 		static const char *const list_SECURITY_LABEL[] =
 		{"TABLE", "COLUMN", "AGGREGATE", "DATABASE", "DOMAIN",
 			"EVENT TRIGGER", "FOREIGN TABLE", "FUNCTION", "LARGE OBJECT",
-			"MATERIALIZED VIEW", "LANGUAGE", "PUBLICATION", "ROLE", "SCHEMA",
+			"MATERIALIZED VIEW", "LANGUAGE", "PUBLICATION", "PROCEDURE", "ROLE", "ROUTINE", "SCHEMA",
 		"SEQUENCE", "SUBSCRIPTION", "TABLESPACE", "TYPE", "VIEW", NULL};
 
 		COMPLETE_WITH_LIST(list_SECURITY_LABEL);
@@ -3233,8 +3282,8 @@ psql_completion(const char *text, int start, int end)
 	/* Complete SET <var> with "TO" */
 	else if (Matches2("SET", MatchAny))
 		COMPLETE_WITH_CONST("TO");
-	/* Complete ALTER DATABASE|FUNCTION|ROLE|USER ... SET <name> */
-	else if (HeadMatches2("ALTER", "DATABASE|FUNCTION|ROLE|USER") &&
+	/* Complete ALTER DATABASE|FUNCTION||PROCEDURE|ROLE|ROUTINE|USER ... SET <name> */
+	else if (HeadMatches2("ALTER", "DATABASE|FUNCTION|PROCEDURE|ROLE|ROUTINE|USER") &&
 			 TailMatches2("SET", MatchAny))
 		COMPLETE_WITH_LIST2("FROM CURRENT", "TO");
 	/* Suggest possible variable values */
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index bfead9af3d..52cbf61ccb 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -59,12 +59,13 @@ extern void DropTransformById(Oid transformOid);
 extern void IsThereFunctionInNamespace(const char *proname, int pronargs,
 						   oidvector *proargtypes, Oid nspOid);
 extern void ExecuteDoStmt(DoStmt *stmt);
+extern void ExecuteCallStmt(ParseState *pstate, CallStmt *stmt);
 extern Oid	get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok);
 extern Oid	get_transform_oid(Oid type_id, Oid lang_id, bool missing_ok);
 extern void interpret_function_parameter_list(ParseState *pstate,
 								  List *parameters,
 								  Oid languageOid,
-								  bool is_aggregate,
+								  ObjectType objtype,
 								  oidvector **parameterTypes,
 								  ArrayType **allParameterTypes,
 								  ArrayType **parameterModes,
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 03dc5307e8..c5b5115f5b 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -414,6 +414,7 @@ typedef enum NodeTag
 	T_DropSubscriptionStmt,
 	T_CreateStatsStmt,
 	T_AlterCollationStmt,
+	T_CallStmt,
 
 	/*
 	 * TAGS FOR PARSE TREE NODES (parsenodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 34d6afc80f..c4ff1c5544 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -355,6 +355,7 @@ typedef struct FuncCall
 	bool		agg_distinct;	/* arguments were labeled DISTINCT */
 	bool		func_variadic;	/* last argument was labeled VARIADIC */
 	struct WindowDef *over;		/* OVER clause, if any */
+	bool		proc_call;		/* CALL statement */
 	int			location;		/* token location, or -1 if unknown */
 } FuncCall;
 
@@ -1642,9 +1643,11 @@ typedef enum ObjectType
 	OBJECT_OPERATOR,
 	OBJECT_OPFAMILY,
 	OBJECT_POLICY,
+	OBJECT_PROCEDURE,
 	OBJECT_PUBLICATION,
 	OBJECT_PUBLICATION_REL,
 	OBJECT_ROLE,
+	OBJECT_ROUTINE,
 	OBJECT_RULE,
 	OBJECT_SCHEMA,
 	OBJECT_SEQUENCE,
@@ -1856,6 +1859,8 @@ typedef enum GrantObjectType
 	ACL_OBJECT_LANGUAGE,		/* procedural language */
 	ACL_OBJECT_LARGEOBJECT,		/* largeobject */
 	ACL_OBJECT_NAMESPACE,		/* namespace */
+	ACL_OBJECT_PROCEDURE,		/* procedure */
+	ACL_OBJECT_ROUTINE,			/* routine */
 	ACL_OBJECT_TABLESPACE,		/* tablespace */
 	ACL_OBJECT_TYPE				/* type */
 } GrantObjectType;
@@ -2749,6 +2754,7 @@ typedef struct CreateFunctionStmt
 	List	   *funcname;		/* qualified name of function to create */
 	List	   *parameters;		/* a list of FunctionParameter */
 	TypeName   *returnType;		/* the return type */
+	bool		is_procedure;
 	List	   *options;		/* a list of DefElem */
 	List	   *withClause;		/* a list of DefElem */
 } CreateFunctionStmt;
@@ -2775,6 +2781,7 @@ typedef struct FunctionParameter
 typedef struct AlterFunctionStmt
 {
 	NodeTag		type;
+	ObjectType	objtype;
 	ObjectWithArgs *func;		/* name and args of function */
 	List	   *actions;		/* list of DefElem */
 } AlterFunctionStmt;
@@ -2799,6 +2806,16 @@ typedef struct InlineCodeBlock
 	bool		langIsTrusted;	/* trusted property of the language */
 } InlineCodeBlock;
 
+/* ----------------------
+ *		CALL statement
+ * ----------------------
+ */
+typedef struct CallStmt
+{
+	NodeTag		type;
+	FuncCall   *funccall;
+} CallStmt;
+
 /* ----------------------
  *		Alter Object Rename Statement
  * ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f50e45e886..a932400058 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -63,6 +63,7 @@ PG_KEYWORD("boolean", BOOLEAN_P, COL_NAME_KEYWORD)
 PG_KEYWORD("both", BOTH, RESERVED_KEYWORD)
 PG_KEYWORD("by", BY, UNRESERVED_KEYWORD)
 PG_KEYWORD("cache", CACHE, UNRESERVED_KEYWORD)
+PG_KEYWORD("call", CALL, UNRESERVED_KEYWORD)
 PG_KEYWORD("called", CALLED, UNRESERVED_KEYWORD)
 PG_KEYWORD("cascade", CASCADE, UNRESERVED_KEYWORD)
 PG_KEYWORD("cascaded", CASCADED, UNRESERVED_KEYWORD)
@@ -310,6 +311,7 @@ PG_KEYWORD("prior", PRIOR, UNRESERVED_KEYWORD)
 PG_KEYWORD("privileges", PRIVILEGES, UNRESERVED_KEYWORD)
 PG_KEYWORD("procedural", PROCEDURAL, UNRESERVED_KEYWORD)
 PG_KEYWORD("procedure", PROCEDURE, UNRESERVED_KEYWORD)
+PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD)
 PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD)
 PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD)
 PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD)
@@ -340,6 +342,8 @@ PG_KEYWORD("right", RIGHT, TYPE_FUNC_NAME_KEYWORD)
 PG_KEYWORD("role", ROLE, UNRESERVED_KEYWORD)
 PG_KEYWORD("rollback", ROLLBACK, UNRESERVED_KEYWORD)
 PG_KEYWORD("rollup", ROLLUP, UNRESERVED_KEYWORD)
+PG_KEYWORD("routine", ROUTINE, UNRESERVED_KEYWORD)
+PG_KEYWORD("routines", ROUTINES, UNRESERVED_KEYWORD)
 PG_KEYWORD("row", ROW, COL_NAME_KEYWORD)
 PG_KEYWORD("rows", ROWS, UNRESERVED_KEYWORD)
 PG_KEYWORD("rule", RULE, UNRESERVED_KEYWORD)
diff --git a/src/include/parser/parse_func.h b/src/include/parser/parse_func.h
index b4b6084b1b..fccccd21ed 100644
--- a/src/include/parser/parse_func.h
+++ b/src/include/parser/parse_func.h
@@ -24,6 +24,7 @@ typedef enum
 	FUNCDETAIL_NOTFOUND,		/* no matching function */
 	FUNCDETAIL_MULTIPLE,		/* too many matching functions */
 	FUNCDETAIL_NORMAL,			/* found a matching regular function */
+	FUNCDETAIL_PROCEDURE,		/* found a matching procedure */
 	FUNCDETAIL_AGGREGATE,		/* found a matching aggregate function */
 	FUNCDETAIL_WINDOWFUNC,		/* found a matching window function */
 	FUNCDETAIL_COERCION			/* it's a type coercion request */
@@ -31,7 +32,8 @@ typedef enum
 
 
 extern Node *ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
-				  Node *last_srf, FuncCall *fn, int location);
+							   Node *last_srf, FuncCall *fn, bool proc_call,
+							   int location);
 
 extern FuncDetailCode func_get_detail(List *funcname,
 				List *fargs, List *fargnames,
@@ -62,10 +64,8 @@ extern const char *func_signature_string(List *funcname, int nargs,
 
 extern Oid LookupFuncName(List *funcname, int nargs, const Oid *argtypes,
 			   bool noError);
-extern Oid LookupFuncWithArgs(ObjectWithArgs *func,
+extern Oid LookupFuncWithArgs(ObjectType objtype, ObjectWithArgs *func,
 				   bool noError);
-extern Oid LookupAggWithArgs(ObjectWithArgs *agg,
-				  bool noError);
 
 extern void check_srf_call_placement(ParseState *pstate, Node *last_srf,
 						 int location);
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f0e210ad8d..565bb3dc6c 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -67,7 +67,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_EXECUTE_PARAMETER,	/* parameter value in EXECUTE */
 	EXPR_KIND_TRIGGER_WHEN,		/* WHEN condition in CREATE TRIGGER */
 	EXPR_KIND_POLICY,			/* USING or WITH CHECK expr in policy */
-	EXPR_KIND_PARTITION_EXPRESSION	/* PARTITION BY expression */
+	EXPR_KIND_PARTITION_EXPRESSION,	/* PARTITION BY expression */
+	EXPR_KIND_CALL				/* CALL argument */
 } ParseExprKind;
 
 
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 07208b56ce..b316cc594c 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -118,6 +118,7 @@ extern bool get_func_retset(Oid funcid);
 extern bool func_strict(Oid funcid);
 extern char func_volatile(Oid funcid);
 extern char func_parallel(Oid funcid);
+extern bool get_func_isagg(Oid funcid);
 extern bool get_func_leakproof(Oid funcid);
 extern float4 get_func_cost(Oid funcid);
 extern float4 get_func_rows(Oid funcid);
diff --git a/src/interfaces/ecpg/preproc/ecpg.tokens b/src/interfaces/ecpg/preproc/ecpg.tokens
index 68ba925efe..1d613af02f 100644
--- a/src/interfaces/ecpg/preproc/ecpg.tokens
+++ b/src/interfaces/ecpg/preproc/ecpg.tokens
@@ -2,7 +2,7 @@
 
 /* special embedded SQL tokens */
 %token  SQL_ALLOCATE SQL_AUTOCOMMIT SQL_BOOL SQL_BREAK
-                SQL_CALL SQL_CARDINALITY SQL_CONNECT
+                SQL_CARDINALITY SQL_CONNECT
                 SQL_COUNT
                 SQL_DATETIME_INTERVAL_CODE
                 SQL_DATETIME_INTERVAL_PRECISION SQL_DESCRIBE
diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer
index f60a62099d..19dc781885 100644
--- a/src/interfaces/ecpg/preproc/ecpg.trailer
+++ b/src/interfaces/ecpg/preproc/ecpg.trailer
@@ -1460,13 +1460,13 @@ action : CONTINUE_P
 			$<action>$.command = NULL;
 			$<action>$.str = mm_strdup("continue");
 		}
-		| SQL_CALL name '(' c_args ')'
+		| CALL name '(' c_args ')'
 		{
 			$<action>$.code = W_DO;
 			$<action>$.command = cat_str(4, $2, mm_strdup("("), $4, mm_strdup(")"));
 			$<action>$.str = cat2_str(mm_strdup("call"), mm_strdup($<action>$.command));
 		}
-		| SQL_CALL name
+		| CALL name
 		{
 			$<action>$.code = W_DO;
 			$<action>$.command = cat2_str($2, mm_strdup("()"));
@@ -1482,7 +1482,6 @@ ECPGKeywords: ECPGKeywords_vanames	{ $$ = $1; }
 		;
 
 ECPGKeywords_vanames:  SQL_BREAK		{ $$ = mm_strdup("break"); }
-		| SQL_CALL						{ $$ = mm_strdup("call"); }
 		| SQL_CARDINALITY				{ $$ = mm_strdup("cardinality"); }
 		| SQL_COUNT						{ $$ = mm_strdup("count"); }
 		| SQL_DATETIME_INTERVAL_CODE	{ $$ = mm_strdup("datetime_interval_code"); }
diff --git a/src/interfaces/ecpg/preproc/ecpg_keywords.c b/src/interfaces/ecpg/preproc/ecpg_keywords.c
index 3b52b8f3a2..848b2d4849 100644
--- a/src/interfaces/ecpg/preproc/ecpg_keywords.c
+++ b/src/interfaces/ecpg/preproc/ecpg_keywords.c
@@ -33,7 +33,6 @@ static const ScanKeyword ECPGScanKeywords[] = {
 	{"autocommit", SQL_AUTOCOMMIT, 0},
 	{"bool", SQL_BOOL, 0},
 	{"break", SQL_BREAK, 0},
-	{"call", SQL_CALL, 0},
 	{"cardinality", SQL_CARDINALITY, 0},
 	{"connect", SQL_CONNECT, 0},
 	{"count", SQL_COUNT, 0},
diff --git a/src/pl/plperl/GNUmakefile b/src/pl/plperl/GNUmakefile
index 91d1296b21..b829027d05 100644
--- a/src/pl/plperl/GNUmakefile
+++ b/src/pl/plperl/GNUmakefile
@@ -55,7 +55,7 @@ endif # win32
 SHLIB_LINK = $(perl_embed_ldflags)
 
 REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=plperl  --load-extension=plperlu
-REGRESS = plperl plperl_lc plperl_trigger plperl_shared plperl_elog plperl_util plperl_init plperlu plperl_array
+REGRESS = plperl plperl_lc plperl_trigger plperl_shared plperl_elog plperl_util plperl_init plperlu plperl_array plperl_call
 # if Perl can support two interpreters in one backend,
 # test plperl-and-plperlu cases
 ifneq ($(PERL),)
diff --git a/src/pl/plperl/expected/plperl_call.out b/src/pl/plperl/expected/plperl_call.out
new file mode 100644
index 0000000000..4bccfcb7c8
--- /dev/null
+++ b/src/pl/plperl/expected/plperl_call.out
@@ -0,0 +1,29 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE plperl
+AS $$
+undef;
+$$;
+CALL test_proc1();
+CREATE PROCEDURE test_proc2()
+LANGUAGE plperl
+AS $$
+return 5
+$$;
+CALL test_proc2();
+CREATE TABLE test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plperl
+AS $$
+spi_exec_query("INSERT INTO test1 VALUES ($_[0])");
+$$;
+CALL test_proc3(55);
+SELECT * FROM test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE test1;
diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index a57393fbdd..9f5313235f 100644
--- a/src/pl/plperl/plperl.c
+++ b/src/pl/plperl/plperl.c
@@ -1915,7 +1915,7 @@ plperl_inline_handler(PG_FUNCTION_ARGS)
 	desc.fn_retistuple = false;
 	desc.fn_retisset = false;
 	desc.fn_retisarray = false;
-	desc.result_oid = VOIDOID;
+	desc.result_oid = InvalidOid;
 	desc.nargs = 0;
 	desc.reference = NULL;
 
@@ -2481,7 +2481,7 @@ plperl_func_handler(PG_FUNCTION_ARGS)
 		}
 		retval = (Datum) 0;
 	}
-	else
+	else if (prodesc->result_oid)
 	{
 		retval = plperl_sv_to_datum(perlret,
 									prodesc->result_oid,
@@ -2826,7 +2826,7 @@ compile_plperl_function(Oid fn_oid, bool is_trigger, bool is_event_trigger)
 		 * Get the required information for input conversion of the
 		 * return value.
 		 ************************************************************/
-		if (!is_trigger && !is_event_trigger)
+		if (!is_trigger && !is_event_trigger && procStruct->prorettype)
 		{
 			Oid			rettype = procStruct->prorettype;
 
@@ -3343,7 +3343,7 @@ plperl_return_next_internal(SV *sv)
 
 		tuplestore_puttuple(current_call_data->tuple_store, tuple);
 	}
-	else
+	else if (prodesc->result_oid)
 	{
 		Datum		ret[1];
 		bool		isNull[1];
diff --git a/src/pl/plperl/sql/plperl_call.sql b/src/pl/plperl/sql/plperl_call.sql
new file mode 100644
index 0000000000..bd2b63b418
--- /dev/null
+++ b/src/pl/plperl/sql/plperl_call.sql
@@ -0,0 +1,36 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE plperl
+AS $$
+undef;
+$$;
+
+CALL test_proc1();
+
+
+CREATE PROCEDURE test_proc2()
+LANGUAGE plperl
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plperl
+AS $$
+spi_exec_query("INSERT INTO test1 VALUES ($_[0])");
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index d0afa59242..f459c02f7b 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -275,7 +275,6 @@ do_compile(FunctionCallInfo fcinfo,
 	bool		isnull;
 	char	   *proc_source;
 	HeapTuple	typeTup;
-	Form_pg_type typeStruct;
 	PLpgSQL_variable *var;
 	PLpgSQL_rec *rec;
 	int			i;
@@ -531,53 +530,58 @@ do_compile(FunctionCallInfo fcinfo,
 			/*
 			 * Lookup the function's return type
 			 */
-			typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rettypeid));
-			if (!HeapTupleIsValid(typeTup))
-				elog(ERROR, "cache lookup failed for type %u", rettypeid);
-			typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
-
-			/* Disallow pseudotype result, except VOID or RECORD */
-			/* (note we already replaced polymorphic types) */
-			if (typeStruct->typtype == TYPTYPE_PSEUDO)
+			if (rettypeid)
 			{
-				if (rettypeid == VOIDOID ||
-					rettypeid == RECORDOID)
-					 /* okay */ ;
-				else if (rettypeid == TRIGGEROID || rettypeid == EVTTRIGGEROID)
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("trigger functions can only be called as triggers")));
-				else
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("PL/pgSQL functions cannot return type %s",
-									format_type_be(rettypeid))));
-			}
+				Form_pg_type typeStruct;
 
-			if (typeStruct->typrelid != InvalidOid ||
-				rettypeid == RECORDOID)
-				function->fn_retistuple = true;
-			else
-			{
-				function->fn_retbyval = typeStruct->typbyval;
-				function->fn_rettyplen = typeStruct->typlen;
+				typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rettypeid));
+				if (!HeapTupleIsValid(typeTup))
+					elog(ERROR, "cache lookup failed for type %u", rettypeid);
+				typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
 
-				/*
-				 * install $0 reference, but only for polymorphic return
-				 * types, and not when the return is specified through an
-				 * output parameter.
-				 */
-				if (IsPolymorphicType(procStruct->prorettype) &&
-					num_out_args == 0)
+				/* Disallow pseudotype result, except VOID or RECORD */
+				/* (note we already replaced polymorphic types) */
+				if (typeStruct->typtype == TYPTYPE_PSEUDO)
 				{
-					(void) plpgsql_build_variable("$0", 0,
-												  build_datatype(typeTup,
-																 -1,
-																 function->fn_input_collation),
-												  true);
+					if (rettypeid == VOIDOID ||
+						rettypeid == RECORDOID)
+						/* okay */ ;
+					else if (rettypeid == TRIGGEROID || rettypeid == EVTTRIGGEROID)
+						ereport(ERROR,
+								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+								 errmsg("trigger functions can only be called as triggers")));
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+								 errmsg("PL/pgSQL functions cannot return type %s",
+										format_type_be(rettypeid))));
+				}
+
+				if (typeStruct->typrelid != InvalidOid ||
+					rettypeid == RECORDOID)
+					function->fn_retistuple = true;
+				else
+				{
+					function->fn_retbyval = typeStruct->typbyval;
+					function->fn_rettyplen = typeStruct->typlen;
+
+					/*
+					 * install $0 reference, but only for polymorphic return
+					 * types, and not when the return is specified through an
+					 * output parameter.
+					 */
+					if (IsPolymorphicType(procStruct->prorettype) &&
+						num_out_args == 0)
+					{
+						(void) plpgsql_build_variable("$0", 0,
+													  build_datatype(typeTup,
+																	 -1,
+																	 function->fn_input_collation),
+													  true);
+					}
 				}
+				ReleaseSysCache(typeTup);
 			}
-			ReleaseSysCache(typeTup);
 			break;
 
 		case PLPGSQL_DML_TRIGGER:
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 7a6dd15460..882b16e2b1 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -462,7 +462,7 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo,
 	estate.err_text = NULL;
 	estate.err_stmt = (PLpgSQL_stmt *) (func->action);
 	rc = exec_stmt_block(&estate, func->action);
-	if (rc != PLPGSQL_RC_RETURN)
+	if (rc != PLPGSQL_RC_RETURN && func->fn_rettype)
 	{
 		estate.err_stmt = NULL;
 		estate.err_text = NULL;
@@ -509,6 +509,12 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo,
 	}
 	else if (!estate.retisnull)
 	{
+		if (!func->fn_rettype)
+		{
+			ereport(ERROR,
+					(errmsg("cannot return a value from a procedure")));
+		}
+
 		if (estate.retistuple)
 		{
 			/*
diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile
index 7680d49cb6..cc91afebde 100644
--- a/src/pl/plpython/Makefile
+++ b/src/pl/plpython/Makefile
@@ -78,6 +78,7 @@ REGRESS = \
 	plpython_spi \
 	plpython_newline \
 	plpython_void \
+	plpython_call \
 	plpython_params \
 	plpython_setof \
 	plpython_record \
diff --git a/src/pl/plpython/expected/plpython_call.out b/src/pl/plpython/expected/plpython_call.out
new file mode 100644
index 0000000000..90785343b6
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_call.out
@@ -0,0 +1,35 @@
+--
+-- Tests for procedures / CALL syntax
+--
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpythonu
+AS $$
+pass
+$$;
+CALL test_proc1();
+-- error: can't return non-None
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpythonu
+AS $$
+return 5
+$$;
+CALL test_proc2();
+ERROR:  PL/Python procedure did not return None
+CONTEXT:  PL/Python procedure "test_proc2"
+CREATE TABLE test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpythonu
+AS $$
+plpy.execute("INSERT INTO test1 VALUES (%s)" % x)
+$$;
+CALL test_proc3(55);
+SELECT * FROM test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE test1;
diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
index 9d2341a4a3..2c304053c1 100644
--- a/src/pl/plpython/plpy_exec.c
+++ b/src/pl/plpython/plpy_exec.c
@@ -197,12 +197,19 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 		error_context_stack = &plerrcontext;
 
 		/*
-		 * If the function is declared to return void, the Python return value
+		 * For a procedure or function declared to return void, the Python return value
 		 * must be None. For void-returning functions, we also treat a None
 		 * return value as a special "void datum" rather than NULL (as is the
 		 * case for non-void-returning functions).
 		 */
-		if (proc->result.typoid == VOIDOID)
+		if (proc->is_procedure)
+		{
+			if (plrv != Py_None)
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("PL/Python procedure did not return None")));
+		}
+		else if (proc->result.typoid == VOIDOID)
 		{
 			if (plrv != Py_None)
 				ereport(ERROR,
@@ -670,7 +677,8 @@ plpython_return_error_callback(void *arg)
 {
 	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 
-	if (exec_ctx->curr_proc)
+	if (exec_ctx->curr_proc &&
+		!exec_ctx->curr_proc->is_procedure)
 		errcontext("while creating return value");
 }
 
diff --git a/src/pl/plpython/plpy_main.c b/src/pl/plpython/plpy_main.c
index 32d23ae5b6..695de30583 100644
--- a/src/pl/plpython/plpy_main.c
+++ b/src/pl/plpython/plpy_main.c
@@ -389,8 +389,14 @@ plpython_error_callback(void *arg)
 	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 
 	if (exec_ctx->curr_proc)
-		errcontext("PL/Python function \"%s\"",
-				   PLy_procedure_name(exec_ctx->curr_proc));
+	{
+		if (exec_ctx->curr_proc->is_procedure)
+			errcontext("PL/Python procedure \"%s\"",
+					   PLy_procedure_name(exec_ctx->curr_proc));
+		else
+			errcontext("PL/Python function \"%s\"",
+					   PLy_procedure_name(exec_ctx->curr_proc));
+	}
 }
 
 static void
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index faa4977463..b7c24e356f 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -189,6 +189,7 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 		proc->fn_tid = procTup->t_self;
 		proc->fn_readonly = (procStruct->provolatile != PROVOLATILE_VOLATILE);
 		proc->is_setof = procStruct->proretset;
+		proc->is_procedure = (procStruct->prorettype == InvalidOid);
 		proc->src = NULL;
 		proc->argnames = NULL;
 		proc->args = NULL;
@@ -206,9 +207,9 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 
 		/*
 		 * get information required for output conversion of the return value,
-		 * but only if this isn't a trigger.
+		 * but only if this isn't a trigger or procedure.
 		 */
-		if (!is_trigger)
+		if (!is_trigger && procStruct->prorettype)
 		{
 			Oid			rettype = procStruct->prorettype;
 			HeapTuple	rvTypeTup;
diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h
index cd1b87fdc3..8968b5c92e 100644
--- a/src/pl/plpython/plpy_procedure.h
+++ b/src/pl/plpython/plpy_procedure.h
@@ -30,7 +30,8 @@ typedef struct PLyProcedure
 	TransactionId fn_xmin;
 	ItemPointerData fn_tid;
 	bool		fn_readonly;
-	bool		is_setof;		/* true, if procedure returns result set */
+	bool		is_setof;		/* true, if function returns result set */
+	bool		is_procedure;
 	PLyObToDatum result;		/* Function result output conversion info */
 	PLyDatumToOb result_in;		/* For converting input tuples in a trigger */
 	char	   *src;			/* textual procedure code, after mangling */
diff --git a/src/pl/plpython/sql/plpython_call.sql b/src/pl/plpython/sql/plpython_call.sql
new file mode 100644
index 0000000000..3fb74de5f0
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_call.sql
@@ -0,0 +1,41 @@
+--
+-- Tests for procedures / CALL syntax
+--
+
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpythonu
+AS $$
+pass
+$$;
+
+CALL test_proc1();
+
+
+-- error: can't return non-None
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpythonu
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpythonu
+AS $$
+plpy.execute("INSERT INTO test1 VALUES (%s)" % x)
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/pl/tcl/Makefile b/src/pl/tcl/Makefile
index b8971d3cc8..6a92a9b6aa 100644
--- a/src/pl/tcl/Makefile
+++ b/src/pl/tcl/Makefile
@@ -28,7 +28,7 @@ DATA = pltcl.control pltcl--1.0.sql pltcl--unpackaged--1.0.sql \
        pltclu.control pltclu--1.0.sql pltclu--unpackaged--1.0.sql
 
 REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=pltcl
-REGRESS = pltcl_setup pltcl_queries pltcl_start_proc pltcl_subxact pltcl_unicode
+REGRESS = pltcl_setup pltcl_queries pltcl_call pltcl_start_proc pltcl_subxact pltcl_unicode
 
 # Tcl on win32 ships with import libraries only for Microsoft Visual C++,
 # which are not compatible with mingw gcc. Therefore we need to build a
diff --git a/src/pl/tcl/expected/pltcl_call.out b/src/pl/tcl/expected/pltcl_call.out
new file mode 100644
index 0000000000..7221a37ad0
--- /dev/null
+++ b/src/pl/tcl/expected/pltcl_call.out
@@ -0,0 +1,29 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE pltcl
+AS $$
+unset
+$$;
+CALL test_proc1();
+CREATE PROCEDURE test_proc2()
+LANGUAGE pltcl
+AS $$
+return 5
+$$;
+CALL test_proc2();
+CREATE TABLE test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE pltcl
+AS $$
+spi_exec "INSERT INTO test1 VALUES ($1)"
+$$;
+CALL test_proc3(55);
+SELECT * FROM test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE test1;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index 6d97ddc99b..e0792d93e1 100644
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -146,6 +146,7 @@ typedef struct pltcl_proc_desc
 	Oid			result_typid;	/* OID of fn's result type */
 	FmgrInfo	result_in_func; /* input function for fn's result type */
 	Oid			result_typioparam;	/* param to pass to same */
+	bool		fn_is_procedure;/* true if this is a procedure */
 	bool		fn_retisset;	/* true if function returns a set */
 	bool		fn_retistuple;	/* true if function returns composite */
 	bool		fn_retisdomain; /* true if function returns domain */
@@ -968,7 +969,7 @@ pltcl_func_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
 		retval = (Datum) 0;
 		fcinfo->isnull = true;
 	}
-	else if (fcinfo->isnull)
+	else if (fcinfo->isnull && !prodesc->fn_is_procedure)
 	{
 		retval = InputFunctionCall(&prodesc->result_in_func,
 								   NULL,
@@ -1026,11 +1027,13 @@ pltcl_func_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
 									   call_state);
 		retval = HeapTupleGetDatum(tup);
 	}
-	else
+	else if (!prodesc->fn_is_procedure)
 		retval = InputFunctionCall(&prodesc->result_in_func,
 								   utf_u2e(Tcl_GetStringResult(interp)),
 								   prodesc->result_typioparam,
 								   -1);
+	else
+		retval = 0;
 
 	return retval;
 }
@@ -1506,7 +1509,9 @@ compile_pltcl_function(Oid fn_oid, Oid tgreloid,
 		 * Get the required information for input conversion of the
 		 * return value.
 		 ************************************************************/
-		if (!is_trigger && !is_event_trigger)
+		prodesc->fn_is_procedure = (procStruct->prorettype == InvalidOid);
+
+		if (!is_trigger && !is_event_trigger && procStruct->prorettype)
 		{
 			Oid			rettype = procStruct->prorettype;
 
@@ -2199,7 +2204,7 @@ pltcl_returnnext(ClientData cdata, Tcl_Interp *interp,
 				tuplestore_puttuple(call_state->tuple_store, tuple);
 			}
 		}
-		else
+		else if (!prodesc->fn_is_procedure)
 		{
 			Datum		retval;
 			bool		isNull = false;
diff --git a/src/pl/tcl/sql/pltcl_call.sql b/src/pl/tcl/sql/pltcl_call.sql
new file mode 100644
index 0000000000..ef1f540f50
--- /dev/null
+++ b/src/pl/tcl/sql/pltcl_call.sql
@@ -0,0 +1,36 @@
+CREATE PROCEDURE test_proc1()
+LANGUAGE pltcl
+AS $$
+unset
+$$;
+
+CALL test_proc1();
+
+
+CREATE PROCEDURE test_proc2()
+LANGUAGE pltcl
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE pltcl
+AS $$
+spi_exec "INSERT INTO test1 VALUES ($1)"
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/test/regress/expected/create_procedure.out b/src/test/regress/expected/create_procedure.out
new file mode 100644
index 0000000000..5538ef2f2b
--- /dev/null
+++ b/src/test/regress/expected/create_procedure.out
@@ -0,0 +1,92 @@
+CALL nonexistent();  -- error
+ERROR:  function nonexistent() does not exist
+LINE 1: CALL nonexistent();
+             ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+CALL random();  -- error
+ERROR:  random() is not a procedure
+LINE 1: CALL random();
+             ^
+HINT:  To call a function, use SELECT.
+CREATE FUNCTION testfunc1(a int) RETURNS int LANGUAGE SQL AS $$ SELECT a $$;
+CREATE TABLE cp_test (a int, b text);
+CREATE PROCEDURE ptest1(x text)
+LANGUAGE SQL
+AS $$
+INSERT INTO cp_test VALUES (1, x);
+$$;
+SELECT ptest1('x');  -- error
+ERROR:  ptest1(unknown) is a procedure
+LINE 1: SELECT ptest1('x');
+               ^
+HINT:  To call a procedure, use CALL.
+CALL ptest1('a');  -- ok
+\df ptest1
+                        List of functions
+ Schema |  Name  | Result data type | Argument data types | Type 
+--------+--------+------------------+---------------------+------
+ public | ptest1 |                  | x text              | proc
+(1 row)
+
+SELECT * FROM cp_test ORDER BY a;
+ a | b 
+---+---
+ 1 | a
+(1 row)
+
+CREATE PROCEDURE ptest2()
+LANGUAGE SQL
+AS $$
+SELECT 5;
+$$;
+CALL ptest2();
+-- various error cases
+CREATE PROCEDURE ptestx() LANGUAGE SQL WINDOW AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+ERROR:  invalid attribute in procedure definition
+LINE 1: CREATE PROCEDURE ptestx() LANGUAGE SQL WINDOW AS $$ INSERT I...
+                                               ^
+CREATE PROCEDURE ptestx() LANGUAGE SQL STRICT AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+ERROR:  invalid attribute in procedure definition
+LINE 1: CREATE PROCEDURE ptestx() LANGUAGE SQL STRICT AS $$ INSERT I...
+                                               ^
+CREATE PROCEDURE ptestx(OUT a int) LANGUAGE SQL AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+ERROR:  procedures cannot have OUT parameters
+ALTER PROCEDURE ptest1(text) STRICT;
+ERROR:  invalid attribute in procedure definition
+LINE 1: ALTER PROCEDURE ptest1(text) STRICT;
+                                     ^
+ALTER FUNCTION ptest1(text) VOLATILE;  -- error: not a function
+ERROR:  ptest1(text) is not a function
+ALTER PROCEDURE testfunc1(int) VOLATILE;  -- error: not a procedure
+ERROR:  testfunc1(integer) is not a procedure
+ALTER PROCEDURE nonexistent() VOLATILE;
+ERROR:  procedure nonexistent() does not exist
+DROP FUNCTION ptest1(text);  -- error: not a function
+ERROR:  ptest1(text) is not a function
+DROP PROCEDURE testfunc1(int);  -- error: not a procedure
+ERROR:  testfunc1(integer) is not a procedure
+DROP PROCEDURE nonexistent();
+ERROR:  procedure nonexistent() does not exist
+-- privileges
+CREATE USER regress_user1;
+GRANT INSERT ON cp_test TO regress_user1;
+REVOKE EXECUTE ON PROCEDURE ptest1(text) FROM PUBLIC;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- error
+ERROR:  permission denied for function ptest1
+RESET ROLE;
+GRANT EXECUTE ON PROCEDURE ptest1(text) TO regress_user1;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- ok
+RESET ROLE;
+-- ROUTINE syntax
+ALTER ROUTINE testfunc1(int) RENAME TO testfunc1a;
+ALTER ROUTINE testfunc1a RENAME TO testfunc1;
+ALTER ROUTINE ptest1(text) RENAME TO ptest1a;
+ALTER ROUTINE ptest1a RENAME TO ptest1;
+DROP ROUTINE testfunc1(int);
+-- cleanup
+DROP PROCEDURE ptest1;
+DROP PROCEDURE ptest2;
+DROP TABLE cp_test;
+DROP USER regress_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 1fdadbc9ef..bfd9d54c11 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -29,6 +29,7 @@ CREATE DOMAIN addr_nsp.gendomain AS int4 CONSTRAINT domconstr CHECK (value > 0);
 CREATE FUNCTION addr_nsp.trig() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN END; $$;
 CREATE TRIGGER t BEFORE INSERT ON addr_nsp.gentable FOR EACH ROW EXECUTE PROCEDURE addr_nsp.trig();
 CREATE POLICY genpol ON addr_nsp.gentable;
+CREATE PROCEDURE addr_nsp.proc(int4) LANGUAGE SQL AS $$ $$;
 CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
@@ -88,7 +89,7 @@ BEGIN
 		('table'), ('index'), ('sequence'), ('view'),
 		('materialized view'), ('foreign table'),
 		('table column'), ('foreign table column'),
-		('aggregate'), ('function'), ('type'), ('cast'),
+		('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
 		('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
 		('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
 		('text search parser'), ('text search dictionary'),
@@ -171,6 +172,12 @@ WARNING:  error for function,{addr_nsp,zwei},{}: function addr_nsp.zwei() does n
 WARNING:  error for function,{addr_nsp,zwei},{integer}: function addr_nsp.zwei(integer) does not exist
 WARNING:  error for function,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for function,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for procedure,{eins},{}: procedure eins() does not exist
+WARNING:  error for procedure,{eins},{integer}: procedure eins(integer) does not exist
+WARNING:  error for procedure,{addr_nsp,zwei},{}: procedure addr_nsp.zwei() does not exist
+WARNING:  error for procedure,{addr_nsp,zwei},{integer}: procedure addr_nsp.zwei(integer) does not exist
+WARNING:  error for procedure,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for procedure,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for type,{eins},{}: type "eins" does not exist
 WARNING:  error for type,{eins},{integer}: type "eins" does not exist
 WARNING:  error for type,{addr_nsp,zwei},{}: name list length must be exactly 1
@@ -371,6 +378,7 @@ WITH objects (type, name, args) AS (VALUES
 				('foreign table column', '{addr_nsp, genftable, a}', '{}'),
 				('aggregate', '{addr_nsp, genaggr}', '{int4}'),
 				('function', '{pg_catalog, pg_identify_object}', '{pg_catalog.oid, pg_catalog.oid, int4}'),
+				('procedure', '{addr_nsp, proc}', '{int4}'),
 				('type', '{pg_catalog._int4}', '{}'),
 				('type', '{addr_nsp.gendomain}', '{}'),
 				('type', '{addr_nsp.gencomptype}', '{}'),
@@ -431,6 +439,7 @@ SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*,
  type                      | addr_nsp   | gendomain         | addr_nsp.gendomain                                                   | t
  function                  | pg_catalog |                   | pg_catalog.pg_identify_object(pg_catalog.oid,pg_catalog.oid,integer) | t
  aggregate                 | addr_nsp   |                   | addr_nsp.genaggr(integer)                                            | t
+ procedure                 | addr_nsp   |                   | addr_nsp.proc(integer)                                               | t
  sequence                  | addr_nsp   | gentable_a_seq    | addr_nsp.gentable_a_seq                                              | t
  table                     | addr_nsp   | gentable          | addr_nsp.gentable                                                    | t
  table column              | addr_nsp   | gentable          | addr_nsp.gentable.b                                                  | t
@@ -469,7 +478,7 @@ SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*,
  subscription              |            | addr_sub          | addr_sub                                                             | t
  publication               |            | addr_pub          | addr_pub                                                             | t
  publication relation      |            |                   | gentable in publication addr_pub                                     | t
-(46 rows)
+(47 rows)
 
 ---
 --- Cleanup resources
@@ -480,6 +489,6 @@ NOTICE:  drop cascades to 4 other objects
 DROP PUBLICATION addr_pub;
 DROP SUBSCRIPTION addr_sub;
 DROP SCHEMA addr_nsp CASCADE;
-NOTICE:  drop cascades to 12 other objects
+NOTICE:  drop cascades to 13 other objects
 DROP OWNED BY regress_addr_user;
 DROP USER regress_addr_user;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index bb3532676b..d6e5bc3353 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -6040,3 +6040,44 @@ END; $$ LANGUAGE plpgsql;
 ERROR:  "x" is not a scalar variable
 LINE 3:   GET DIAGNOSTICS x = ROW_COUNT;
                           ^
+--
+-- Procedures
+--
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    NULL;
+END;
+$$;
+CALL test_proc1();
+-- error: can't return non-NULL
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN 5;
+END;
+$$;
+CALL test_proc2();
+ERROR:  cannot return a value from a procedure
+CONTEXT:  PL/pgSQL function test_proc2() while casting return value to function's return type
+CREATE TABLE proc_test1 (a int);
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    INSERT INTO proc_test1 VALUES (x);
+END;
+$$;
+CALL test_proc3(55);
+SELECT * FROM proc_test1;
+ a  
+----
+ 55
+(1 row)
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE proc_test1;
diff --git a/src/test/regress/expected/polymorphism.out b/src/test/regress/expected/polymorphism.out
index 91cfb743b6..66e35a6a5c 100644
--- a/src/test/regress/expected/polymorphism.out
+++ b/src/test/regress/expected/polymorphism.out
@@ -915,10 +915,10 @@ select dfunc();
 
 -- verify it lists properly
 \df dfunc
-                                           List of functions
- Schema | Name  | Result data type |                    Argument data types                    |  Type  
---------+-------+------------------+-----------------------------------------------------------+--------
- public | dfunc | integer          | a integer DEFAULT 1, OUT sum integer, b integer DEFAULT 2 | normal
+                                          List of functions
+ Schema | Name  | Result data type |                    Argument data types                    | Type 
+--------+-------+------------------+-----------------------------------------------------------+------
+ public | dfunc | integer          | a integer DEFAULT 1, OUT sum integer, b integer DEFAULT 2 | func
 (1 row)
 
 drop function dfunc(int, int);
@@ -1083,10 +1083,10 @@ $$ select array_upper($1, 1) $$ language sql;
 ERROR:  cannot remove parameter defaults from existing function
 HINT:  Use DROP FUNCTION dfunc(integer[]) first.
 \df dfunc
-                                      List of functions
- Schema | Name  | Result data type |               Argument data types               |  Type  
---------+-------+------------------+-------------------------------------------------+--------
- public | dfunc | integer          | VARIADIC a integer[] DEFAULT ARRAY[]::integer[] | normal
+                                     List of functions
+ Schema | Name  | Result data type |               Argument data types               | Type 
+--------+-------+------------------+-------------------------------------------------+------
+ public | dfunc | integer          | VARIADIC a integer[] DEFAULT ARRAY[]::integer[] | func
 (1 row)
 
 drop function dfunc(a variadic int[]);
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 771971a095..e6994f0490 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -651,13 +651,25 @@ GRANT USAGE ON LANGUAGE sql TO regress_user2; -- fail
 WARNING:  no privileges were granted for "sql"
 CREATE FUNCTION testfunc1(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql;
 CREATE FUNCTION testfunc2(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
-REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int) FROM PUBLIC;
-GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int) TO regress_user2;
+CREATE AGGREGATE testagg1(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testproc1(int) AS 'select $1;' LANGUAGE sql;
+REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) TO regress_user2;
+REVOKE ALL ON FUNCTION testproc1(int) FROM PUBLIC; -- fail, not a function
+ERROR:  testproc1(integer) is not a function
+REVOKE ALL ON PROCEDURE testproc1(int) FROM PUBLIC;
+GRANT EXECUTE ON PROCEDURE testproc1(int) TO regress_user2;
 GRANT USAGE ON FUNCTION testfunc1(int) TO regress_user3; -- semantic error
 ERROR:  invalid privilege type USAGE for function
+GRANT USAGE ON FUNCTION testagg1(int) TO regress_user3; -- semantic error
+ERROR:  invalid privilege type USAGE for function
+GRANT USAGE ON PROCEDURE testproc1(int) TO regress_user3; -- semantic error
+ERROR:  invalid privilege type USAGE for procedure
 GRANT ALL PRIVILEGES ON FUNCTION testfunc1(int) TO regress_user4;
 GRANT ALL PRIVILEGES ON FUNCTION testfunc_nosuch(int) TO regress_user4;
 ERROR:  function testfunc_nosuch(integer) does not exist
+GRANT ALL PRIVILEGES ON FUNCTION testagg1(int) TO regress_user4;
+GRANT ALL PRIVILEGES ON PROCEDURE testproc1(int) TO regress_user4;
 CREATE FUNCTION testfunc4(boolean) RETURNS text
   AS 'select col1 from atest2 where col2 = $1;'
   LANGUAGE sql SECURITY DEFINER;
@@ -671,9 +683,20 @@ SELECT testfunc1(5), testfunc2(5); -- ok
 
 CREATE FUNCTION testfunc3(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql; -- fail
 ERROR:  permission denied for language sql
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
+ testagg1 
+----------
+        6
+(1 row)
+
+CALL testproc1(6); -- ok
 SET SESSION AUTHORIZATION regress_user3;
 SELECT testfunc1(5); -- fail
 ERROR:  permission denied for function testfunc1
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- fail
+ERROR:  permission denied for function testagg1
+CALL testproc1(6); -- fail
+ERROR:  permission denied for function testproc1
 SELECT col1 FROM atest2 WHERE col2 = true; -- fail
 ERROR:  permission denied for relation atest2
 SELECT testfunc4(true); -- ok
@@ -689,8 +712,19 @@ SELECT testfunc1(5); -- ok
         10
 (1 row)
 
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
+ testagg1 
+----------
+        6
+(1 row)
+
+CALL testproc1(6); -- ok
 DROP FUNCTION testfunc1(int); -- fail
 ERROR:  must be owner of function testfunc1
+DROP AGGREGATE testagg1(int); -- fail
+ERROR:  must be owner of function testagg1
+DROP PROCEDURE testproc1(int); -- fail
+ERROR:  must be owner of function testproc1
 \c -
 DROP FUNCTION testfunc1(int); -- ok
 -- restore to sanity
@@ -1537,22 +1571,54 @@ SELECT has_schema_privilege('regress_user2', 'testns5', 'CREATE'); -- no
 
 SET ROLE regress_user1;
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testns.bar() AS 'select 1' LANGUAGE sql;
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- no
  has_function_privilege 
 ------------------------
  f
 (1 row)
 
-ALTER DEFAULT PRIVILEGES IN SCHEMA testns GRANT EXECUTE ON FUNCTIONS to public;
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- no
+ has_function_privilege 
+------------------------
+ f
+(1 row)
+
+SELECT has_function_privilege('regress_user2', 'testns.bar()', 'EXECUTE'); -- no
+ has_function_privilege 
+------------------------
+ f
+(1 row)
+
+ALTER DEFAULT PRIVILEGES IN SCHEMA testns GRANT EXECUTE ON ROUTINES to public;
 DROP FUNCTION testns.foo();
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+DROP AGGREGATE testns.agg1(int);
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
+DROP PROCEDURE testns.bar();
+CREATE PROCEDURE testns.bar() AS 'select 1' LANGUAGE sql;
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- yes
  has_function_privilege 
 ------------------------
  t
 (1 row)
 
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- yes
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
+SELECT has_function_privilege('regress_user2', 'testns.bar()', 'EXECUTE'); -- yes (counts as function here)
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
 DROP FUNCTION testns.foo();
+DROP AGGREGATE testns.agg1(int);
+DROP PROCEDURE testns.bar();
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_user1 REVOKE USAGE ON TYPES FROM public;
 CREATE DOMAIN testns.testdomain1 AS int;
 SELECT has_type_privilege('regress_user2', 'testns.testdomain1', 'USAGE'); -- no
@@ -1631,12 +1697,26 @@ SELECT has_table_privilege('regress_user1', 'testns.t2', 'SELECT'); -- false
 (1 row)
 
 CREATE FUNCTION testns.testfunc(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
+CREATE AGGREGATE testns.testagg(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testns.testproc(int) AS 'select 3' LANGUAGE sql;
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- true by default
  has_function_privilege 
 ------------------------
  t
 (1 row)
 
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- true by default
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- true by default
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
 REVOKE ALL ON ALL FUNCTIONS IN SCHEMA testns FROM PUBLIC;
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- false
  has_function_privilege 
@@ -1644,9 +1724,47 @@ SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'
  f
 (1 row)
 
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- false
+ has_function_privilege 
+------------------------
+ f
+(1 row)
+
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- still true, not a function
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
+REVOKE ALL ON ALL PROCEDURES IN SCHEMA testns FROM PUBLIC;
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- now false
+ has_function_privilege 
+------------------------
+ f
+(1 row)
+
+GRANT ALL ON ALL ROUTINES IN SCHEMA testns TO PUBLIC;
+SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- true
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- true
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- true
+ has_function_privilege 
+------------------------
+ t
+(1 row)
+
 \set VERBOSITY terse \\ -- suppress cascade details
 DROP SCHEMA testns CASCADE;
-NOTICE:  drop cascades to 3 other objects
+NOTICE:  drop cascades to 5 other objects
 \set VERBOSITY default
 -- Change owner of the schema & and rename of new schema owner
 \c -
@@ -1729,8 +1847,10 @@ drop table dep_priv_test;
 -- clean up
 \c
 drop sequence x_seq;
+DROP AGGREGATE testagg1(int);
 DROP FUNCTION testfunc2(int);
 DROP FUNCTION testfunc4(boolean);
+DROP PROCEDURE testproc1(int);
 DROP VIEW atestv0;
 DROP VIEW atestv1;
 DROP VIEW atestv2;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1a3ac4c1f9..22f79b1410 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -53,7 +53,7 @@ test: copy copyselect copydml
 # ----------
 # More groups of parallel tests
 # ----------
-test: create_misc create_operator
+test: create_misc create_operator create_procedure
 # These depend on the above two
 test: create_index create_view
 
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index a205e5d05c..1dc1ce73ea 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -63,6 +63,7 @@ test: copyselect
 test: copydml
 test: create_misc
 test: create_operator
+test: create_procedure
 test: create_index
 test: create_view
 test: create_aggregate
diff --git a/src/test/regress/sql/create_procedure.sql b/src/test/regress/sql/create_procedure.sql
new file mode 100644
index 0000000000..f09ba2ad30
--- /dev/null
+++ b/src/test/regress/sql/create_procedure.sql
@@ -0,0 +1,79 @@
+CALL nonexistent();  -- error
+CALL random();  -- error
+
+CREATE FUNCTION testfunc1(a int) RETURNS int LANGUAGE SQL AS $$ SELECT a $$;
+
+CREATE TABLE cp_test (a int, b text);
+
+CREATE PROCEDURE ptest1(x text)
+LANGUAGE SQL
+AS $$
+INSERT INTO cp_test VALUES (1, x);
+$$;
+
+SELECT ptest1('x');  -- error
+CALL ptest1('a');  -- ok
+
+\df ptest1
+
+SELECT * FROM cp_test ORDER BY a;
+
+
+CREATE PROCEDURE ptest2()
+LANGUAGE SQL
+AS $$
+SELECT 5;
+$$;
+
+CALL ptest2();
+
+
+-- various error cases
+
+CREATE PROCEDURE ptestx() LANGUAGE SQL WINDOW AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+CREATE PROCEDURE ptestx() LANGUAGE SQL STRICT AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+CREATE PROCEDURE ptestx(OUT a int) LANGUAGE SQL AS $$ INSERT INTO cp_test VALUES (1, 'a') $$;
+
+ALTER PROCEDURE ptest1(text) STRICT;
+ALTER FUNCTION ptest1(text) VOLATILE;  -- error: not a function
+ALTER PROCEDURE testfunc1(int) VOLATILE;  -- error: not a procedure
+ALTER PROCEDURE nonexistent() VOLATILE;
+
+DROP FUNCTION ptest1(text);  -- error: not a function
+DROP PROCEDURE testfunc1(int);  -- error: not a procedure
+DROP PROCEDURE nonexistent();
+
+
+-- privileges
+
+CREATE USER regress_user1;
+GRANT INSERT ON cp_test TO regress_user1;
+REVOKE EXECUTE ON PROCEDURE ptest1(text) FROM PUBLIC;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- error
+RESET ROLE;
+GRANT EXECUTE ON PROCEDURE ptest1(text) TO regress_user1;
+SET ROLE regress_user1;
+CALL ptest1('a');  -- ok
+RESET ROLE;
+
+
+-- ROUTINE syntax
+
+ALTER ROUTINE testfunc1(int) RENAME TO testfunc1a;
+ALTER ROUTINE testfunc1a RENAME TO testfunc1;
+
+ALTER ROUTINE ptest1(text) RENAME TO ptest1a;
+ALTER ROUTINE ptest1a RENAME TO ptest1;
+
+DROP ROUTINE testfunc1(int);
+
+
+-- cleanup
+
+DROP PROCEDURE ptest1;
+DROP PROCEDURE ptest2;
+
+DROP TABLE cp_test;
+
+DROP USER regress_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 63821b8008..55faa71edf 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -32,6 +32,7 @@ CREATE DOMAIN addr_nsp.gendomain AS int4 CONSTRAINT domconstr CHECK (value > 0);
 CREATE FUNCTION addr_nsp.trig() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN END; $$;
 CREATE TRIGGER t BEFORE INSERT ON addr_nsp.gentable FOR EACH ROW EXECUTE PROCEDURE addr_nsp.trig();
 CREATE POLICY genpol ON addr_nsp.gentable;
+CREATE PROCEDURE addr_nsp.proc(int4) LANGUAGE SQL AS $$ $$;
 CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
@@ -81,7 +82,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 		('table'), ('index'), ('sequence'), ('view'),
 		('materialized view'), ('foreign table'),
 		('table column'), ('foreign table column'),
-		('aggregate'), ('function'), ('type'), ('cast'),
+		('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
 		('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
 		('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
 		('text search parser'), ('text search dictionary'),
@@ -147,6 +148,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 				('foreign table column', '{addr_nsp, genftable, a}', '{}'),
 				('aggregate', '{addr_nsp, genaggr}', '{int4}'),
 				('function', '{pg_catalog, pg_identify_object}', '{pg_catalog.oid, pg_catalog.oid, int4}'),
+				('procedure', '{addr_nsp, proc}', '{int4}'),
 				('type', '{pg_catalog._int4}', '{}'),
 				('type', '{addr_nsp.gendomain}', '{}'),
 				('type', '{addr_nsp.gencomptype}', '{}'),
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index 6620ea6172..1c355132b7 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -4820,3 +4820,52 @@ CREATE FUNCTION fx(x WSlot) RETURNS void AS $$
   GET DIAGNOSTICS x = ROW_COUNT;
   RETURN;
 END; $$ LANGUAGE plpgsql;
+
+
+--
+-- Procedures
+--
+
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    NULL;
+END;
+$$;
+
+CALL test_proc1();
+
+
+-- error: can't return non-NULL
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN 5;
+END;
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE proc_test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    INSERT INTO proc_test1 VALUES (x);
+END;
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM proc_test1;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE proc_test1;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index a900ba2f84..ea8dd028cd 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -442,12 +442,21 @@ CREATE TABLE atestc (fz int) INHERITS (atestp1, atestp2);
 GRANT USAGE ON LANGUAGE sql TO regress_user2; -- fail
 CREATE FUNCTION testfunc1(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql;
 CREATE FUNCTION testfunc2(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
-
-REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int) FROM PUBLIC;
-GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int) TO regress_user2;
+CREATE AGGREGATE testagg1(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testproc1(int) AS 'select $1;' LANGUAGE sql;
+
+REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION testfunc1(int), testfunc2(int), testagg1(int) TO regress_user2;
+REVOKE ALL ON FUNCTION testproc1(int) FROM PUBLIC; -- fail, not a function
+REVOKE ALL ON PROCEDURE testproc1(int) FROM PUBLIC;
+GRANT EXECUTE ON PROCEDURE testproc1(int) TO regress_user2;
 GRANT USAGE ON FUNCTION testfunc1(int) TO regress_user3; -- semantic error
+GRANT USAGE ON FUNCTION testagg1(int) TO regress_user3; -- semantic error
+GRANT USAGE ON PROCEDURE testproc1(int) TO regress_user3; -- semantic error
 GRANT ALL PRIVILEGES ON FUNCTION testfunc1(int) TO regress_user4;
 GRANT ALL PRIVILEGES ON FUNCTION testfunc_nosuch(int) TO regress_user4;
+GRANT ALL PRIVILEGES ON FUNCTION testagg1(int) TO regress_user4;
+GRANT ALL PRIVILEGES ON PROCEDURE testproc1(int) TO regress_user4;
 
 CREATE FUNCTION testfunc4(boolean) RETURNS text
   AS 'select col1 from atest2 where col2 = $1;'
@@ -457,16 +466,24 @@ CREATE FUNCTION testfunc4(boolean) RETURNS text
 SET SESSION AUTHORIZATION regress_user2;
 SELECT testfunc1(5), testfunc2(5); -- ok
 CREATE FUNCTION testfunc3(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql; -- fail
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
+CALL testproc1(6); -- ok
 
 SET SESSION AUTHORIZATION regress_user3;
 SELECT testfunc1(5); -- fail
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- fail
+CALL testproc1(6); -- fail
 SELECT col1 FROM atest2 WHERE col2 = true; -- fail
 SELECT testfunc4(true); -- ok
 
 SET SESSION AUTHORIZATION regress_user4;
 SELECT testfunc1(5); -- ok
+SELECT testagg1(x) FROM (VALUES (1), (2), (3)) _(x); -- ok
+CALL testproc1(6); -- ok
 
 DROP FUNCTION testfunc1(int); -- fail
+DROP AGGREGATE testagg1(int); -- fail
+DROP PROCEDURE testproc1(int); -- fail
 
 \c -
 
@@ -931,17 +948,29 @@ CREATE SCHEMA testns5;
 SET ROLE regress_user1;
 
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testns.bar() AS 'select 1' LANGUAGE sql;
 
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- no
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- no
+SELECT has_function_privilege('regress_user2', 'testns.bar()', 'EXECUTE'); -- no
 
-ALTER DEFAULT PRIVILEGES IN SCHEMA testns GRANT EXECUTE ON FUNCTIONS to public;
+ALTER DEFAULT PRIVILEGES IN SCHEMA testns GRANT EXECUTE ON ROUTINES to public;
 
 DROP FUNCTION testns.foo();
 CREATE FUNCTION testns.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+DROP AGGREGATE testns.agg1(int);
+CREATE AGGREGATE testns.agg1(int) (sfunc = int4pl, stype = int4);
+DROP PROCEDURE testns.bar();
+CREATE PROCEDURE testns.bar() AS 'select 1' LANGUAGE sql;
 
 SELECT has_function_privilege('regress_user2', 'testns.foo()', 'EXECUTE'); -- yes
+SELECT has_function_privilege('regress_user2', 'testns.agg1(int)', 'EXECUTE'); -- yes
+SELECT has_function_privilege('regress_user2', 'testns.bar()', 'EXECUTE'); -- yes (counts as function here)
 
 DROP FUNCTION testns.foo();
+DROP AGGREGATE testns.agg1(int);
+DROP PROCEDURE testns.bar();
 
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_user1 REVOKE USAGE ON TYPES FROM public;
 
@@ -995,12 +1024,28 @@ CREATE TABLE testns.t2 (f1 int);
 SELECT has_table_privilege('regress_user1', 'testns.t2', 'SELECT'); -- false
 
 CREATE FUNCTION testns.testfunc(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql;
+CREATE AGGREGATE testns.testagg(int) (sfunc = int4pl, stype = int4);
+CREATE PROCEDURE testns.testproc(int) AS 'select 3' LANGUAGE sql;
 
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- true by default
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- true by default
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- true by default
 
 REVOKE ALL ON ALL FUNCTIONS IN SCHEMA testns FROM PUBLIC;
 
 SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- false
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- false
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- still true, not a function
+
+REVOKE ALL ON ALL PROCEDURES IN SCHEMA testns FROM PUBLIC;
+
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- now false
+
+GRANT ALL ON ALL ROUTINES IN SCHEMA testns TO PUBLIC;
+
+SELECT has_function_privilege('regress_user1', 'testns.testfunc(int)', 'EXECUTE'); -- true
+SELECT has_function_privilege('regress_user1', 'testns.testagg(int)', 'EXECUTE'); -- true
+SELECT has_function_privilege('regress_user1', 'testns.testproc(int)', 'EXECUTE'); -- true
 
 \set VERBOSITY terse \\ -- suppress cascade details
 DROP SCHEMA testns CASCADE;
@@ -1064,8 +1109,10 @@ CREATE SCHEMA testns;
 
 drop sequence x_seq;
 
+DROP AGGREGATE testagg1(int);
 DROP FUNCTION testfunc2(int);
 DROP FUNCTION testfunc4(boolean);
+DROP PROCEDURE testproc1(int);
 
 DROP VIEW atestv0;
 DROP VIEW atestv1;

base-commit: 487a0c1518af2f3ae2d05b7fd23d636d687f28f3
-- 
2.15.0

#41Simon Riggs
simon@2ndquadrant.com
In reply to: Peter Eisentraut (#40)
Re: [HACKERS] SQL procedures

On 29 November 2017 at 02:03, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

Here is a new patch that addresses the previous review comments.

If there are no new comments, I think this might be ready to go.

Is ERRCODE_INVALID_FUNCTION_DEFINITION still appropriate?

Other than that, looks ready to go.

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

#42Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Simon Riggs (#41)
Re: [HACKERS] SQL procedures

On 11/28/17 10:34, Simon Riggs wrote:

Is ERRCODE_INVALID_FUNCTION_DEFINITION still appropriate?

Well, maybe not, but changing that is likely more trouble than it's worth.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#43Andrew Dunstan
andrew.dunstan@2ndquadrant.com
In reply to: Peter Eisentraut (#40)
Re: [HACKERS] SQL procedures

On 11/28/2017 10:03 AM, Peter Eisentraut wrote:

Here is a new patch that addresses the previous review comments.

If there are no new comments, I think this might be ready to go.

Looks good to me. Marking ready for committer.

cheers

andrew

--
Andrew Dunstan https://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#44Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Andrew Dunstan (#43)
Re: [HACKERS] SQL procedures

On 11/29/17 14:17, Andrew Dunstan wrote:

On 11/28/2017 10:03 AM, Peter Eisentraut wrote:

Here is a new patch that addresses the previous review comments.

If there are no new comments, I think this might be ready to go.

Looks good to me. Marking ready for committer.

committed

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#45Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Peter Eisentraut (#44)
Re: [HACKERS] SQL procedures

On Fri, Dec 1, 2017 at 7:48 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

On 11/29/17 14:17, Andrew Dunstan wrote:

On 11/28/2017 10:03 AM, Peter Eisentraut wrote:

Here is a new patch that addresses the previous review comments.

If there are no new comments, I think this might be ready to go.

Looks good to me. Marking ready for committer.

committed

postgres=# \df
List of functions
Schema | Name | Result data type | Argument data types | Type
--------+------+------------------+---------------------+------
public | bar | integer | i integer | func
public | foo | | i integer | proc
(2 rows)

Should this now be called a "List of routines"?

--
Thomas Munro
http://www.enterprisedb.com

#46Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Thomas Munro (#45)
Re: [HACKERS] SQL procedures

On 11/30/17 15:50, Thomas Munro wrote:

postgres=# \df
List of functions
Schema | Name | Result data type | Argument data types | Type
--------+------+------------------+---------------------+------
public | bar | integer | i integer | func
public | foo | | i integer | proc
(2 rows)

Should this now be called a "List of routines"?

Maybe, but I hesitate to go around and change all mentions of "function"
like that. That might just confuse people.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#47Robert Haas
robertmhaas@gmail.com
In reply to: Peter Eisentraut (#46)
Re: [HACKERS] SQL procedures

On Fri, Dec 1, 2017 at 9:31 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

On 11/30/17 15:50, Thomas Munro wrote:

postgres=# \df
List of functions
Schema | Name | Result data type | Argument data types | Type
--------+------+------------------+---------------------+------
public | bar | integer | i integer | func
public | foo | | i integer | proc
(2 rows)

Should this now be called a "List of routines"?

Maybe, but I hesitate to go around and change all mentions of "function"
like that. That might just confuse people.

Yeah, this is not unlike the problems we have deciding whether to say
"relation" or "table". It's a problem that comes when most foos are
bars but there are multiple types of exotic foo that are not bars.
That's pretty much the case here -- most functions are probably just
functions, but a few might be procedures or aggregates. I think
leaving this and similar cases as "functions" is fine. I wonder
whether it was really necessary for the SQL standards committee (or
Oracle) to invent both procedures and functions to represent very
similar things, but they did.

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

#48Simon Riggs
simon@2ndquadrant.com
In reply to: Peter Eisentraut (#46)
Re: [HACKERS] SQL procedures

On 2 December 2017 at 01:31, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

On 11/30/17 15:50, Thomas Munro wrote:

postgres=# \df
List of functions
Schema | Name | Result data type | Argument data types | Type
--------+------+------------------+---------------------+------
public | bar | integer | i integer | func
public | foo | | i integer | proc
(2 rows)

Should this now be called a "List of routines"?

Maybe, but I hesitate to go around and change all mentions of "function"
like that. That might just confuse people.

Agreed

\dfn shows functions only
so we might want to consider having
\df say "List of functions and procedures"
\dfn say "List of functions"

and a new option to list procedures only
\dfp say "List of procedures"

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

#49Robert Haas
robertmhaas@gmail.com
In reply to: Peter Eisentraut (#7)
Re: [HACKERS] SQL procedures

On Wed, Nov 8, 2017 at 9:21 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

Why not use VOIDOID for the prorettype value?

We need a way to distinguish functions that are callable by SELECT and
procedures that are callable by CALL.

I agree that we need this, but using prorettype = InvalidOid to do it
might not be the best way, because it only works for procedures that
don't return anything. If a procedure could return, say, an integer,
then it would fail, because we have two ways to say that something in
pg_proc returns nothing (InvalidOid, VOIDOID) but we have only one way
to say that it returns an integer (INT4OID). Similarly, we'd have a
problem if we ever tried to use procedures for handling triggers or
(perhaps more likely) event triggers, since those special kinds of
functions also signal their purpose via the return type - which may
also not have been such a hot idea, but at least in those cases the
idea of returning any sort of real result is more or less known to be
nonsensical.

Anyway, I think it would be better to invent an explicit way to
represent whether something is a procedure rather than relying on
overloading prorettype to tell us.

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

#50Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#49)
Re: [HACKERS] SQL procedures

Robert Haas <robertmhaas@gmail.com> writes:

I agree that we need this, but using prorettype = InvalidOid to do it
might not be the best way, because it only works for procedures that
don't return anything. If a procedure could return, say, an integer,

Good point, because that is possible in some other systems, and so
somebody is going to ask for it at some point.

Anyway, I think it would be better to invent an explicit way to
represent whether something is a procedure rather than relying on
overloading prorettype to tell us.

+1 --- seems like a new bool column is the thing. Or may we should merge
"proisprocedure" with proisagg and proiswindow into an enum prokind?
Although that would break some existing client-side code.

PS: I still strongly disagree with allowing prorettype to be zero.

regards, tom lane

#51Pavel Stehule
pavel.stehule@gmail.com
In reply to: Tom Lane (#50)
Re: [HACKERS] SQL procedures

2018-01-02 17:47 GMT+01:00 Tom Lane <tgl@sss.pgh.pa.us>:

Robert Haas <robertmhaas@gmail.com> writes:

I agree that we need this, but using prorettype = InvalidOid to do it
might not be the best way, because it only works for procedures that
don't return anything. If a procedure could return, say, an integer,

Good point, because that is possible in some other systems, and so
somebody is going to ask for it at some point.

Anyway, I think it would be better to invent an explicit way to
represent whether something is a procedure rather than relying on
overloading prorettype to tell us.

+1 --- seems like a new bool column is the thing. Or may we should merge
"proisprocedure" with proisagg and proiswindow into an enum prokind?
Although that would break some existing client-side code.

+1

Pavel

Show quoted text

PS: I still strongly disagree with allowing prorettype to be zero.

regards, tom lane

#52Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#50)
Re: [HACKERS] SQL procedures

On Tue, Jan 2, 2018 at 11:47 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Anyway, I think it would be better to invent an explicit way to
represent whether something is a procedure rather than relying on
overloading prorettype to tell us.

+1 --- seems like a new bool column is the thing. Or may we should merge
"proisprocedure" with proisagg and proiswindow into an enum prokind?
Although that would break some existing client-side code.

Assuming that we're confident that "procedure" is mutually exclusive
with "aggregate" and "window function", that seems like the right way
to go. And I think that's probably the case, both because we're
deciding not to let procedures be called from SELECT statements
(though Oracle apparently does) and because we've chosen syntax that
suggests distinctness: CREATE AGGREGATE, CREATE FUNCTION, CREATE
PROCEDURE. Window functions are less distinct from the others, since
they go through CREATE FUNCTION, but it still seems unlikely that we'd
ever want to try to support a "window procedure".

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

#53Andrew Dunstan
andrew.dunstan@2ndquadrant.com
In reply to: Robert Haas (#52)
Re: [HACKERS] SQL procedures

On 01/02/2018 01:45 PM, Robert Haas wrote:

On Tue, Jan 2, 2018 at 11:47 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Anyway, I think it would be better to invent an explicit way to
represent whether something is a procedure rather than relying on
overloading prorettype to tell us.

+1 --- seems like a new bool column is the thing. Or may we should merge
"proisprocedure" with proisagg and proiswindow into an enum prokind?
Although that would break some existing client-side code.

Assuming that we're confident that "procedure" is mutually exclusive
with "aggregate" and "window function", that seems like the right way
to go. And I think that's probably the case, both because we're
deciding not to let procedures be called from SELECT statements
(though Oracle apparently does) and because we've chosen syntax that
suggests distinctness: CREATE AGGREGATE, CREATE FUNCTION, CREATE
PROCEDURE. Window functions are less distinct from the others, since
they go through CREATE FUNCTION, but it still seems unlikely that we'd
ever want to try to support a "window procedure".

Yeah, but these things don't feel like they belong in the same category.
The fact that we have to ask this question is a symptom of that. A
boolean feels more natural to me here, although I acknowledge it will
result in a tiny amount of catalog bloat. Tom's point about client-side
code is also valid. I don't feel very strongly about it, though.

cheers

andrew

--
Andrew Dunstan https://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#54Robert Haas
robertmhaas@gmail.com
In reply to: Andrew Dunstan (#53)
Re: [HACKERS] SQL procedures

On Tue, Jan 2, 2018 at 1:57 PM, Andrew Dunstan
<andrew.dunstan@2ndquadrant.com> wrote:

Yeah, but these things don't feel like they belong in the same category.
The fact that we have to ask this question is a symptom of that.

Well, that's got to be asked about any representation we choose - that
question is the motivation for not liking the use of prorettype for
this purpose, so it's only fair to ask whether any alternative has the
same problem.

A
boolean feels more natural to me here, although I acknowledge it will
result in a tiny amount of catalog bloat. Tom's point about client-side
code is also valid. I don't feel very strongly about it, though.

I think the catalog bloat is too minor to care about, but if these
things really are mutually exclusive, it's more natural to have them
use a single flag character rather than a series of Booleans.
Otherwise, it may be unclear to the casual observer (or hacker) that
at most one of the Booleans can be true, possibly leading to user
confusion (or bugs).

It's pretty well impossible to introduce new features without
occasionally changing the catalog representation. We had several
people grumble when I replaced relistemp with relpersistence, and we
(rightly, IMHO) told those people to suck it up and deal. I don't
think we should react any differently here. I recognize that it's a
pain, but it's not that much of a pain, and it may even be helpful to
tool authors who actually need to handle procedures differently than
functions, which is probably a lot of them. pgAdmin for example seems
like it will certainly need to care.

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

#55Andrew Dunstan
andrew.dunstan@2ndquadrant.com
In reply to: Robert Haas (#54)
Re: [HACKERS] SQL procedures

On 01/02/2018 02:14 PM, Robert Haas wrote:

On Tue, Jan 2, 2018 at 1:57 PM, Andrew Dunstan
<andrew.dunstan@2ndquadrant.com> wrote:

Yeah, but these things don't feel like they belong in the same category.
The fact that we have to ask this question is a symptom of that.

Well, that's got to be asked about any representation we choose - that
question is the motivation for not liking the use of prorettype for
this purpose, so it's only fair to ask whether any alternative has the
same problem.

I think there's broad agreement about not liking use of prorettype for
this purpose.

A
boolean feels more natural to me here, although I acknowledge it will
result in a tiny amount of catalog bloat. Tom's point about client-side
code is also valid. I don't feel very strongly about it, though.

I think the catalog bloat is too minor to care about, but if these
things really are mutually exclusive, it's more natural to have them
use a single flag character rather than a series of Booleans.
Otherwise, it may be unclear to the casual observer (or hacker) that
at most one of the Booleans can be true, possibly leading to user
confusion (or bugs).

Fair point. I don't recall if we discussed anything like this when
window functions were introduced.

It's pretty well impossible to introduce new features without
occasionally changing the catalog representation. We had several
people grumble when I replaced relistemp with relpersistence, and we
(rightly, IMHO) told those people to suck it up and deal. I don't
think we should react any differently here. I recognize that it's a
pain, but it's not that much of a pain, and it may even be helpful to
tool authors who actually need to handle procedures differently than
functions, which is probably a lot of them. pgAdmin for example seems
like it will certainly need to care.

I agree.

cheers

andrew

--
Andrew Dunstan https://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#56Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Tom Lane (#50)
Re: [HACKERS] SQL procedures

On 1/2/18 11:47, Tom Lane wrote:

+1 --- seems like a new bool column is the thing. Or may we should merge
"proisprocedure" with proisagg and proiswindow into an enum prokind?
Although that would break some existing client-side code.

prokind sounds good. I'll look into that.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#57Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Peter Eisentraut (#56)
2 attachment(s)
Re: [HACKERS] SQL procedures

On 1/2/18 17:47, Peter Eisentraut wrote:

On 1/2/18 11:47, Tom Lane wrote:

+1 --- seems like a new bool column is the thing. Or may we should merge
"proisprocedure" with proisagg and proiswindow into an enum prokind?
Although that would break some existing client-side code.

prokind sounds good. I'll look into that.

Here is a patch set for that. (I just kept the pg_proc.h changes
separate for easier viewing.) It's not quite complete; some frontend
code still needs adjusting; but the idea is clear.

I think this would be a pretty good change. It doesn't appear to save a
ton amount of code, although a couple of cases where inconsistent
settings of proisagg and proiswindow had to be handed could be removed.
Because window functions are a separate kind in pg_proc but not in the
object address system, inconsistencies will remain in the system, but I
guess that's just the way it is.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

0001-Add-prokind-column-replacing-proisagg-and-proiswindo.patchtext/plain; charset=UTF-8; name=0001-Add-prokind-column-replacing-proisagg-and-proiswindo.patch; x-mac-creator=0; x-mac-type=0Download
From 27dc62f9ec430f0ad6949e6d18c3c7a69f537444 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Wed, 3 Jan 2018 18:21:20 -0500
Subject: [PATCH 1/2] Add prokind column, replacing proisagg and proiswindow

---
 doc/src/sgml/catalogs.sgml                  | 23 +++++------
 src/backend/catalog/objectaddress.c         |  6 +--
 src/backend/catalog/pg_aggregate.c          |  3 +-
 src/backend/catalog/pg_proc.c               | 36 +++--------------
 src/backend/catalog/system_views.sql        |  8 ++--
 src/backend/commands/functioncmds.c         | 29 +++++---------
 src/backend/commands/proclang.c             |  9 ++---
 src/backend/commands/typecmds.c             |  3 +-
 src/backend/parser/parse_coerce.c           |  3 +-
 src/backend/parser/parse_func.c             | 27 +++++++++----
 src/backend/utils/adt/ruleutils.c           |  6 +--
 src/backend/utils/cache/lsyscache.c         |  4 +-
 src/bin/pg_dump/t/002_pg_dump.pl            |  6 +--
 src/bin/psql/describe.c                     | 60 +++++++++++++++++++++++------
 src/bin/psql/tab-complete.c                 |  2 +-
 src/include/catalog/pg_class.h              |  2 +-
 src/include/catalog/pg_proc.h               | 54 ++++++++++++++------------
 src/include/catalog/pg_proc_fn.h            |  3 +-
 src/test/regress/expected/alter_generic.out |  2 +-
 src/test/regress/expected/opr_sanity.out    | 36 ++++++++---------
 src/test/regress/expected/rules.out         |  8 ++--
 src/test/regress/sql/alter_generic.sql      |  2 +-
 src/test/regress/sql/opr_sanity.sql         | 36 ++++++++---------
 23 files changed, 191 insertions(+), 177 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 3f02202caf..2a50dec06d 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5039,15 +5039,17 @@ <title><structname>pg_proc</structname></title>
   </indexterm>
 
   <para>
-   The catalog <structname>pg_proc</structname> stores information about functions (or procedures).
+   The catalog <structname>pg_proc</structname> stores information about
+   functions, procedures, aggregate functions, and window functions
+   (collectively also known as routines).
    See <xref linkend="sql-createfunction"/>
    and <xref linkend="xfunc"/> for more information.
   </para>
 
   <para>
-   The table contains data for aggregate functions as well as plain functions.
-   If <structfield>proisagg</structfield> is true, there should be a matching
-   row in <structfield>pg_aggregate</structfield>.
+   If <structfield>prokind</structfield> indicates that the entry is for an
+   aggregate function, there should be a matching row in
+   <structfield>pg_aggregate</structfield>.
   </para>
 
   <table>
@@ -5134,17 +5136,12 @@ <title><structname>pg_proc</structname> Columns</title>
      </row>
 
      <row>
-      <entry><structfield>proisagg</structfield></entry>
+      <entry><structfield>prokind</structfield></entry>
       <entry><type>bool</type></entry>
       <entry></entry>
-      <entry>Function is an aggregate function</entry>
-     </row>
-
-     <row>
-      <entry><structfield>proiswindow</structfield></entry>
-      <entry><type>bool</type></entry>
-      <entry></entry>
-      <entry>Function is a window function</entry>
+      <entry><literal>f</literal> for a normal function, <literal>p</literal>
+      for a procedure, <literal>a</literal> for an aggregate function,
+      <literal>w</literal> for a window function</entry>
      </row>
 
      <row>
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index bc999ca3c4..4397ef932d 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -4031,11 +4031,11 @@ getProcedureTypeDescription(StringInfo buffer, Oid procid)
 		elog(ERROR, "cache lookup failed for procedure %u", procid);
 	procForm = (Form_pg_proc) GETSTRUCT(procTup);
 
-	if (procForm->proisagg)
+	if (procForm->prokind == PROKIND_AGGREGATE)
 		appendStringInfoString(buffer, "aggregate");
-	else if (procForm->prorettype == InvalidOid)
+	else if (procForm->prokind == PROKIND_PROCEDURE)
 		appendStringInfoString(buffer, "procedure");
-	else
+	else /* function or window function */
 		appendStringInfoString(buffer, "function");
 
 	ReleaseSysCache(procTup);
diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c
index e801c1ed5c..0080ed4d6d 100644
--- a/src/backend/catalog/pg_aggregate.c
+++ b/src/backend/catalog/pg_aggregate.c
@@ -616,8 +616,7 @@ AggregateCreate(const char *aggName,
 							 InvalidOid,	/* no validator */
 							 "aggregate_dummy", /* placeholder proc */
 							 NULL,	/* probin */
-							 true,	/* isAgg */
-							 false, /* isWindowFunc */
+							 PROKIND_AGGREGATE,
 							 false, /* security invoker (currently not
 									 * definable for agg) */
 							 false, /* isLeakProof */
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 39d5172e97..a5c8bf1b3f 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -74,8 +74,7 @@ ProcedureCreate(const char *procedureName,
 				Oid languageValidator,
 				const char *prosrc,
 				const char *probin,
-				bool isAgg,
-				bool isWindowFunc,
+				char prokind,
 				bool security_definer,
 				bool isLeakProof,
 				bool isStrict,
@@ -335,8 +334,7 @@ ProcedureCreate(const char *procedureName,
 	values[Anum_pg_proc_prorows - 1] = Float4GetDatum(prorows);
 	values[Anum_pg_proc_provariadic - 1] = ObjectIdGetDatum(variadicType);
 	values[Anum_pg_proc_protransform - 1] = ObjectIdGetDatum(InvalidOid);
-	values[Anum_pg_proc_proisagg - 1] = BoolGetDatum(isAgg);
-	values[Anum_pg_proc_proiswindow - 1] = BoolGetDatum(isWindowFunc);
+	values[Anum_pg_proc_prokind - 1] = CharGetDatum(prokind);
 	values[Anum_pg_proc_prosecdef - 1] = BoolGetDatum(security_definer);
 	values[Anum_pg_proc_proleakproof - 1] = BoolGetDatum(isLeakProof);
 	values[Anum_pg_proc_proisstrict - 1] = BoolGetDatum(isStrict);
@@ -536,32 +534,10 @@ ProcedureCreate(const char *procedureName,
 		}
 
 		/* Can't change aggregate or window-function status, either */
-		if (oldproc->proisagg != isAgg)
-		{
-			if (oldproc->proisagg)
-				ereport(ERROR,
-						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("function \"%s\" is an aggregate function",
-								procedureName)));
-			else
-				ereport(ERROR,
-						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("function \"%s\" is not an aggregate function",
-								procedureName)));
-		}
-		if (oldproc->proiswindow != isWindowFunc)
-		{
-			if (oldproc->proiswindow)
-				ereport(ERROR,
-						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("function \"%s\" is a window function",
-								procedureName)));
-			else
-				ereport(ERROR,
-						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("function \"%s\" is not a window function",
-								procedureName)));
-		}
+		if (oldproc->prokind != prokind)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot change routine type")));
 
 		/*
 		 * Do not change existing ownership or permissions, either.  Note
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 5652e9ee6d..5e6e8a64f6 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -332,9 +332,11 @@ CREATE VIEW pg_seclabels AS
 UNION ALL
 SELECT
 	l.objoid, l.classoid, l.objsubid,
-	CASE WHEN pro.proisagg = true THEN 'aggregate'::text
-	     WHEN pro.proisagg = false THEN 'function'::text
-	END AS objtype,
+	CASE pro.prokind
+            WHEN 'a' THEN 'aggregate'::text
+            WHEN 'f' THEN 'function'::text
+            WHEN 'p' THEN 'procedure'::text
+            WHEN 'w' THEN 'window'::text END AS objtype,
 	pro.pronamespace AS objnamespace,
 	CASE WHEN pg_function_is_visible(pro.oid)
 	     THEN quote_ident(pro.proname)
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index 12ab33f418..9d4036da8d 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -1151,8 +1151,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 						   languageValidator,
 						   prosrc_str,	/* converted to text later */
 						   probin_str,	/* converted to text later */
-						   false,	/* not an aggregate */
-						   isWindowFunc,
+						   stmt->is_procedure ? PROKIND_PROCEDURE : (isWindowFunc ? PROKIND_WINDOW : PROKIND_FUNCTION),
 						   security,
 						   isLeakProof,
 						   isStrict,
@@ -1180,7 +1179,7 @@ RemoveFunctionById(Oid funcOid)
 {
 	Relation	relation;
 	HeapTuple	tup;
-	bool		isagg;
+	char		prokind;
 
 	/*
 	 * Delete the pg_proc tuple.
@@ -1191,7 +1190,7 @@ RemoveFunctionById(Oid funcOid)
 	if (!HeapTupleIsValid(tup)) /* should not happen */
 		elog(ERROR, "cache lookup failed for function %u", funcOid);
 
-	isagg = ((Form_pg_proc) GETSTRUCT(tup))->proisagg;
+	prokind = ((Form_pg_proc) GETSTRUCT(tup))->prokind;
 
 	CatalogTupleDelete(relation, &tup->t_self);
 
@@ -1202,7 +1201,7 @@ RemoveFunctionById(Oid funcOid)
 	/*
 	 * If there's a pg_aggregate tuple, delete that too.
 	 */
-	if (isagg)
+	if (prokind == PROKIND_AGGREGATE)
 	{
 		relation = heap_open(AggregateRelationId, RowExclusiveLock);
 
@@ -1257,13 +1256,13 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_PROC,
 					   NameListToString(stmt->func->objname));
 
-	if (procForm->proisagg)
+	if (procForm->prokind == PROKIND_AGGREGATE)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is an aggregate function",
 						NameListToString(stmt->func->objname))));
 
-	is_procedure = (procForm->prorettype == InvalidOid);
+	is_procedure = (procForm->prokind == PROKIND_PROCEDURE);
 
 	/* Examine requested actions. */
 	foreach(l, stmt->actions)
@@ -1579,14 +1578,10 @@ CreateCast(CreateCastStmt *stmt)
 					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 					 errmsg("cast function must not be volatile")));
 #endif
-		if (procstruct->proisagg)
+		if (procstruct->prokind != PROKIND_FUNCTION)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-					 errmsg("cast function must not be an aggregate function")));
-		if (procstruct->proiswindow)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-					 errmsg("cast function must not be a window function")));
+					 errmsg("cast function must be a normal function")));
 		if (procstruct->proretset)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -1831,14 +1826,10 @@ check_transform_function(Form_pg_proc procstruct)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 				 errmsg("transform function must not be volatile")));
-	if (procstruct->proisagg)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-				 errmsg("transform function must not be an aggregate function")));
-	if (procstruct->proiswindow)
+	if (procstruct->prokind != PROKIND_FUNCTION)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-				 errmsg("transform function must not be a window function")));
+				 errmsg("transform function must be a normal function")));
 	if (procstruct->proretset)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/proclang.c b/src/backend/commands/proclang.c
index 9783a162d7..3ee3c4fdea 100644
--- a/src/backend/commands/proclang.c
+++ b/src/backend/commands/proclang.c
@@ -129,8 +129,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt)
 									  F_FMGR_C_VALIDATOR,
 									  pltemplate->tmplhandler,
 									  pltemplate->tmpllibrary,
-									  false,	/* isAgg */
-									  false,	/* isWindowFunc */
+									  PROKIND_FUNCTION,
 									  false,	/* security_definer */
 									  false,	/* isLeakProof */
 									  false,	/* isStrict */
@@ -169,8 +168,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt)
 										  F_FMGR_C_VALIDATOR,
 										  pltemplate->tmplinline,
 										  pltemplate->tmpllibrary,
-										  false,	/* isAgg */
-										  false,	/* isWindowFunc */
+										  PROKIND_FUNCTION,
 										  false,	/* security_definer */
 										  false,	/* isLeakProof */
 										  true, /* isStrict */
@@ -212,8 +210,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt)
 										  F_FMGR_C_VALIDATOR,
 										  pltemplate->tmplvalidator,
 										  pltemplate->tmpllibrary,
-										  false,	/* isAgg */
-										  false,	/* isWindowFunc */
+										  PROKIND_FUNCTION,
 										  false,	/* security_definer */
 										  false,	/* isLeakProof */
 										  true, /* isStrict */
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index a40b3cf752..b368f15d85 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1672,8 +1672,7 @@ makeRangeConstructors(const char *name, Oid namespace,
 								 F_FMGR_INTERNAL_VALIDATOR, /* language validator */
 								 prosrc[i], /* prosrc */
 								 NULL,	/* probin */
-								 false, /* isAgg */
-								 false, /* isWindowFunc */
+								 PROKIND_FUNCTION,
 								 false, /* security_definer */
 								 false, /* leakproof */
 								 false, /* isStrict */
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 085aa8766c..665d3327a0 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -834,8 +834,7 @@ build_coercion_expression(Node *node,
 		 */
 		/* Assert(targetTypeId == procstruct->prorettype); */
 		Assert(!procstruct->proretset);
-		Assert(!procstruct->proisagg);
-		Assert(!procstruct->proiswindow);
+		Assert(procstruct->prokind == PROKIND_FUNCTION);
 		nargs = procstruct->pronargs;
 		Assert(nargs >= 1 && nargs <= 3);
 		/* Assert(procstruct->proargtypes.values[0] == exprType(node)); */
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index ffae0f3cf3..e120c0cd84 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -1614,14 +1614,25 @@ func_get_detail(List *funcname,
 				*argdefaults = defaults;
 			}
 		}
-		if (pform->proisagg)
-			result = FUNCDETAIL_AGGREGATE;
-		else if (pform->proiswindow)
-			result = FUNCDETAIL_WINDOWFUNC;
-		else if (pform->prorettype == InvalidOid)
-			result = FUNCDETAIL_PROCEDURE;
-		else
-			result = FUNCDETAIL_NORMAL;
+
+		switch (pform->prokind)
+		{
+			case PROKIND_AGGREGATE:
+				result = FUNCDETAIL_AGGREGATE;
+				break;
+			case PROKIND_FUNCTION:
+				result = FUNCDETAIL_NORMAL;
+				break;
+			case PROKIND_PROCEDURE:
+				result = FUNCDETAIL_PROCEDURE;
+				break;
+			case PROKIND_WINDOW:
+				result = FUNCDETAIL_WINDOWFUNC;
+				break;
+			default:
+				elog(ERROR, "unrecognized prokind: %c", pform->prokind);
+		}
+
 		ReleaseSysCache(ftup);
 		return result;
 	}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 9cdbb06add..51acd44517 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2464,7 +2464,7 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 	proc = (Form_pg_proc) GETSTRUCT(proctup);
 	name = NameStr(proc->proname);
 
-	if (proc->proisagg)
+	if (proc->prokind == PROKIND_AGGREGATE)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is an aggregate function", name)));
@@ -2488,7 +2488,7 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 	/* Emit some miscellaneous options on one line */
 	oldlen = buf.len;
 
-	if (proc->proiswindow)
+	if (proc->prokind == PROKIND_WINDOW)
 		appendStringInfoString(&buf, " WINDOW");
 	switch (proc->provolatile)
 	{
@@ -2791,7 +2791,7 @@ print_function_arguments(StringInfo buf, HeapTuple proctup,
 	}
 
 	/* Check for special treatment of ordered-set aggregates */
-	if (proc->proisagg)
+	if (proc->prokind == PROKIND_AGGREGATE)
 	{
 		HeapTuple	aggtup;
 		Form_pg_aggregate agg;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index e8aa179347..d9463402f2 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1616,7 +1616,7 @@ func_parallel(Oid funcid)
 
 /*
  * get_func_isagg
- *	   Given procedure id, return the function's proisagg field.
+ *	   Given procedure id, return whether the function is an aggregate.
  */
 bool
 get_func_isagg(Oid funcid)
@@ -1628,7 +1628,7 @@ get_func_isagg(Oid funcid)
 	if (!HeapTupleIsValid(tp))
 		elog(ERROR, "cache lookup failed for function %u", funcid);
 
-	result = ((Form_pg_proc) GETSTRUCT(tp))->proisagg;
+	result = ((Form_pg_proc) GETSTRUCT(tp))->prokind == PROKIND_AGGREGATE;
 	ReleaseSysCache(tp);
 	return result;
 }
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 7cf9bdadb2..95f0b32eee 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -6083,8 +6083,7 @@
 						   prorows,
 						   provariadic,
 						   protransform,
-						   proisagg,
-						   proiswindow,
+						   prokind,
 						   prosecdef,
 						   proleakproof,
 						   proisstrict,
@@ -6116,8 +6115,7 @@
 		\QGRANT SELECT(prorows) ON TABLE pg_proc TO PUBLIC;\E\n.*
 		\QGRANT SELECT(provariadic) ON TABLE pg_proc TO PUBLIC;\E\n.*
 		\QGRANT SELECT(protransform) ON TABLE pg_proc TO PUBLIC;\E\n.*
-		\QGRANT SELECT(proisagg) ON TABLE pg_proc TO PUBLIC;\E\n.*
-		\QGRANT SELECT(proiswindow) ON TABLE pg_proc TO PUBLIC;\E\n.*
+		\QGRANT SELECT(prokind) ON TABLE pg_proc TO PUBLIC;\E\n.*
 		\QGRANT SELECT(prosecdef) ON TABLE pg_proc TO PUBLIC;\E\n.*
 		\QGRANT SELECT(proleakproof) ON TABLE pg_proc TO PUBLIC;\E\n.*
 		\QGRANT SELECT(proisstrict) ON TABLE pg_proc TO PUBLIC;\E\n.*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f2e62946d8..102001baff 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -100,12 +100,20 @@ describeAggregates(const char *pattern, bool verbose, bool showSystem)
 						  "  pg_catalog.format_type(p.proargtypes[0], NULL) AS \"%s\",\n",
 						  gettext_noop("Argument data types"));
 
-	appendPQExpBuffer(&buf,
-					  "  pg_catalog.obj_description(p.oid, 'pg_proc') as \"%s\"\n"
-					  "FROM pg_catalog.pg_proc p\n"
-					  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n"
-					  "WHERE p.proisagg\n",
-					  gettext_noop("Description"));
+	if (pset.sversion >= 110000)
+		appendPQExpBuffer(&buf,
+						  "  pg_catalog.obj_description(p.oid, 'pg_proc') as \"%s\"\n"
+						  "FROM pg_catalog.pg_proc p\n"
+						  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n"
+						  "WHERE p.prokind = 'a'\n",
+						  gettext_noop("Description"));
+	else
+		appendPQExpBuffer(&buf,
+						  "  pg_catalog.obj_description(p.oid, 'pg_proc') as \"%s\"\n"
+						  "FROM pg_catalog.pg_proc p\n"
+						  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n"
+						  "WHERE p.proisagg\n",
+						  gettext_noop("Description"));
 
 	if (!showSystem && !pattern)
 		appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
@@ -346,7 +354,25 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 					  gettext_noop("Schema"),
 					  gettext_noop("Name"));
 
-	if (pset.sversion >= 80400)
+	if (pset.sversion >= 110000)
+		appendPQExpBuffer(&buf,
+						  "  pg_catalog.pg_get_function_result(p.oid) as \"%s\",\n"
+						  "  pg_catalog.pg_get_function_arguments(p.oid) as \"%s\",\n"
+						  " CASE p.prokind\n"
+						  "  WHEN 'a' THEN '%s'\n"
+						  "  WHEN 'w' THEN '%s'\n"
+						  "  WHEN 'p' THEN '%s'\n"
+						  "  ELSE '%s'\n"
+						  " END as \"%s\"",
+						  gettext_noop("Result data type"),
+						  gettext_noop("Argument data types"),
+		/* translator: "agg" is short for "aggregate" */
+						  gettext_noop("agg"),
+						  gettext_noop("window"),
+						  gettext_noop("proc"),
+						  gettext_noop("func"),
+						  gettext_noop("Type"));
+	else if (pset.sversion >= 80400)
 		appendPQExpBuffer(&buf,
 						  "  pg_catalog.pg_get_function_result(p.oid) as \"%s\",\n"
 						  "  pg_catalog.pg_get_function_arguments(p.oid) as \"%s\",\n"
@@ -494,7 +520,10 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 				appendPQExpBufferStr(&buf, "WHERE ");
 				have_where = true;
 			}
-			appendPQExpBufferStr(&buf, "NOT p.proisagg\n");
+			if (pset.sversion >= 110000)
+				appendPQExpBufferStr(&buf, "p.prokind <> 'a'\n");
+			else
+				appendPQExpBufferStr(&buf, "NOT p.proisagg\n");
 		}
 		if (!showTrigger)
 		{
@@ -516,7 +545,10 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 				appendPQExpBufferStr(&buf, "WHERE ");
 				have_where = true;
 			}
-			appendPQExpBufferStr(&buf, "NOT p.proiswindow\n");
+			if (pset.sversion >= 110000)
+				appendPQExpBufferStr(&buf, "p.prokind <> 'w'\n");
+			else
+				appendPQExpBufferStr(&buf, "NOT p.proiswindow\n");
 		}
 	}
 	else
@@ -528,7 +560,10 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* Note: at least one of these must be true ... */
 		if (showAggregate)
 		{
-			appendPQExpBufferStr(&buf, "p.proisagg\n");
+			if (pset.sversion >= 110000)
+				appendPQExpBufferStr(&buf, "p.prokind = 'a'\n");
+			else
+				appendPQExpBufferStr(&buf, "p.proisagg\n");
 			needs_or = true;
 		}
 		if (showTrigger)
@@ -543,7 +578,10 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		{
 			if (needs_or)
 				appendPQExpBufferStr(&buf, "       OR ");
-			appendPQExpBufferStr(&buf, "p.proiswindow\n");
+			if (pset.sversion >= 110000)
+				appendPQExpBufferStr(&buf, "p.prokind = 'w'\n");
+			else
+				appendPQExpBufferStr(&buf, "p.proiswindow\n");
 			needs_or = true;
 		}
 		appendPQExpBufferStr(&buf, "      )\n");
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b51098deca..1146e82719 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -349,7 +349,7 @@ static const SchemaQuery Query_for_list_of_aggregates = {
 	/* catname */
 	"pg_catalog.pg_proc p",
 	/* selcondition */
-	"p.proisagg",
+	"p.prokind = 'a'",
 	/* viscondition */
 	"pg_catalog.pg_function_is_visible(p.oid)",
 	/* namespace */
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index e7049438eb..2e395a3938 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -151,7 +151,7 @@ DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t
 DESCR("");
 DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 22 0 f f f f f f f t n f 3 1 _null_ _null_ _null_));
 DESCR("");
-DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n f 3 1 _null_ _null_ _null_));
+DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f f t n f 3 1 _null_ _null_ _null_));
 DESCR("");
 DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 33 0 t f f f f f f t n f 3 1 _null_ _null_ _null_));
 DESCR("");
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 298e0ae2f0..dfe7900341 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -43,8 +43,7 @@ CATALOG(pg_proc,1255) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81) BKI_SCHEMA_MACRO
 	float4		prorows;		/* estimated # of rows out (if proretset) */
 	Oid			provariadic;	/* element type of variadic array, or 0 */
 	regproc		protransform;	/* transforms calls to it during planning */
-	bool		proisagg;		/* is it an aggregate? */
-	bool		proiswindow;	/* is it a window function? */
+	char		prokind;		/* see PROKIND_ categories below */
 	bool		prosecdef;		/* security definer */
 	bool		proleakproof;	/* is it a leak-proof function? */
 	bool		proisstrict;	/* strict with respect to NULLs? */
@@ -86,7 +85,7 @@ typedef FormData_pg_proc *Form_pg_proc;
  *		compiler constants for pg_proc
  * ----------------
  */
-#define Natts_pg_proc					29
+#define Natts_pg_proc					28
 #define Anum_pg_proc_proname			1
 #define Anum_pg_proc_pronamespace		2
 #define Anum_pg_proc_proowner			3
@@ -95,27 +94,26 @@ typedef FormData_pg_proc *Form_pg_proc;
 #define Anum_pg_proc_prorows			6
 #define Anum_pg_proc_provariadic		7
 #define Anum_pg_proc_protransform		8
-#define Anum_pg_proc_proisagg			9
-#define Anum_pg_proc_proiswindow		10
-#define Anum_pg_proc_prosecdef			11
-#define Anum_pg_proc_proleakproof		12
-#define Anum_pg_proc_proisstrict		13
-#define Anum_pg_proc_proretset			14
-#define Anum_pg_proc_provolatile		15
-#define Anum_pg_proc_proparallel		16
-#define Anum_pg_proc_pronargs			17
-#define Anum_pg_proc_pronargdefaults	18
-#define Anum_pg_proc_prorettype			19
-#define Anum_pg_proc_proargtypes		20
-#define Anum_pg_proc_proallargtypes		21
-#define Anum_pg_proc_proargmodes		22
-#define Anum_pg_proc_proargnames		23
-#define Anum_pg_proc_proargdefaults		24
-#define Anum_pg_proc_protrftypes		25
-#define Anum_pg_proc_prosrc				26
-#define Anum_pg_proc_probin				27
-#define Anum_pg_proc_proconfig			28
-#define Anum_pg_proc_proacl				29
+#define Anum_pg_proc_prokind			9
+#define Anum_pg_proc_prosecdef			10
+#define Anum_pg_proc_proleakproof		11
+#define Anum_pg_proc_proisstrict		12
+#define Anum_pg_proc_proretset			13
+#define Anum_pg_proc_provolatile		14
+#define Anum_pg_proc_proparallel		15
+#define Anum_pg_proc_pronargs			16
+#define Anum_pg_proc_pronargdefaults	17
+#define Anum_pg_proc_prorettype			18
+#define Anum_pg_proc_proargtypes		19
+#define Anum_pg_proc_proallargtypes		20
+#define Anum_pg_proc_proargmodes		21
+#define Anum_pg_proc_proargnames		22
+#define Anum_pg_proc_proargdefaults		23
+#define Anum_pg_proc_protrftypes		24
+#define Anum_pg_proc_prosrc				25
+#define Anum_pg_proc_probin				26
+#define Anum_pg_proc_proconfig			27
+#define Anum_pg_proc_proacl				28
 
 /* ----------------
  *		initial contents of pg_proc
@@ -5531,6 +5529,14 @@ DESCR("list of files in the WAL directory");
 DATA(insert OID = 5028 ( satisfies_hash_partition PGNSP PGUID 12 1 0 2276 0 f f f f f f i s 4 0 16 "26 23 23 2276" _null_ "{i,i,i,v}" _null_ _null_ _null_ satisfies_hash_partition _null_ _null_ _null_ ));
 DESCR("hash partition CHECK constraint");
 
+/*
+ * Symbolic values for prokind column
+ */
+#define PROKIND_FUNCTION 'f'
+#define PROKIND_AGGREGATE 'a'
+#define PROKIND_WINDOW 'w'
+#define PROKIND_PROCEDURE 'p'
+
 /*
  * Symbolic values for provolatile column: these indicate whether the result
  * of a function is dependent *only* on the values of its explicit arguments,
diff --git a/src/include/catalog/pg_proc_fn.h b/src/include/catalog/pg_proc_fn.h
index 098e2e6f07..b66871bc46 100644
--- a/src/include/catalog/pg_proc_fn.h
+++ b/src/include/catalog/pg_proc_fn.h
@@ -27,8 +27,7 @@ extern ObjectAddress ProcedureCreate(const char *procedureName,
 				Oid languageValidator,
 				const char *prosrc,
 				const char *probin,
-				bool isAgg,
-				bool isWindowFunc,
+				char prokind,
 				bool security_definer,
 				bool isLeakProof,
 				bool isStrict,
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 767c09bec5..c4641af276 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -83,7 +83,7 @@ ERROR:  must be owner of function alt_agg3
 ALTER AGGREGATE alt_agg2(int) SET SCHEMA alt_nsp2;  -- failed (name conflict)
 ERROR:  function alt_agg2(integer) already exists in schema "alt_nsp2"
 RESET SESSION AUTHORIZATION;
-SELECT n.nspname, proname, prorettype::regtype, proisagg, a.rolname
+SELECT n.nspname, proname, prorettype::regtype, prokind = 'a' AS proisagg, a.rolname
   FROM pg_proc p, pg_namespace n, pg_authid a
   WHERE p.pronamespace = n.oid AND p.proowner = a.oid
     AND n.nspname IN ('alt_nsp1', 'alt_nsp2')
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 684f7f20a8..25518c9faa 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -88,10 +88,10 @@ WHERE prosrc IS NULL OR prosrc = '' OR prosrc = '-';
 -----+---------
 (0 rows)
 
--- proiswindow shouldn't be set together with proisagg or proretset
+-- proretset should only be set for normal functions
 SELECT p1.oid, p1.proname
 FROM pg_proc AS p1
-WHERE proiswindow AND (proisagg OR proretset);
+WHERE proretset AND prokind != 'f';
  oid | proname 
 -----+---------
 (0 rows)
@@ -154,9 +154,9 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid < p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    (p1.proisagg = false OR p2.proisagg = false) AND
+    (p1.prokind != 'a' OR p2.prokind != 'a') AND
     (p1.prolang != p2.prolang OR
-     p1.proisagg != p2.proisagg OR
+     p1.prokind != p2.prokind OR
      p1.prosecdef != p2.prosecdef OR
      p1.proleakproof != p2.proleakproof OR
      p1.proisstrict != p2.proisstrict OR
@@ -182,7 +182,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.prorettype < p2.prorettype)
@@ -198,7 +198,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.proargtypes[0] < p2.proargtypes[0])
@@ -216,7 +216,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.proargtypes[1] < p2.proargtypes[1])
@@ -233,7 +233,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[2] < p2.proargtypes[2])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -246,7 +246,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[3] < p2.proargtypes[3])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -259,7 +259,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[4] < p2.proargtypes[4])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -271,7 +271,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[5] < p2.proargtypes[5])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -283,7 +283,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[6] < p2.proargtypes[6])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -295,7 +295,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[7] < p2.proargtypes[7])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -1286,7 +1286,7 @@ WHERE aggfnoid = 0 OR aggtransfn = 0 OR
 SELECT a.aggfnoid::oid, p.proname
 FROM pg_aggregate as a, pg_proc as p
 WHERE a.aggfnoid = p.oid AND
-    (NOT p.proisagg OR p.proretset OR p.pronargs < a.aggnumdirectargs);
+    (p.prokind != 'a' OR p.proretset OR p.pronargs < a.aggnumdirectargs);
  aggfnoid | proname 
 ----------+---------
 (0 rows)
@@ -1294,7 +1294,7 @@ WHERE a.aggfnoid = p.oid AND
 -- Make sure there are no proisagg pg_proc entries without matches.
 SELECT oid, proname
 FROM pg_proc as p
-WHERE p.proisagg AND
+WHERE p.prokind = 'a' AND
     NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid);
  oid | proname 
 -----+---------
@@ -1633,7 +1633,7 @@ ORDER BY 1, 2;
 SELECT p1.oid::regprocedure, p2.oid::regprocedure
 FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid < p2.oid AND p1.proname = p2.proname AND
-    p1.proisagg AND p2.proisagg AND
+    p1.prokind = 'a' AND p2.prokind = 'a' AND
     array_dims(p1.proargtypes) != array_dims(p2.proargtypes)
 ORDER BY 1;
      oid      |   oid   
@@ -1644,7 +1644,7 @@ ORDER BY 1;
 -- For the same reason, built-in aggregates with default arguments are no good.
 SELECT oid, proname
 FROM pg_proc AS p
-WHERE proisagg AND proargdefaults IS NOT NULL;
+WHERE prokind = 'a' AND proargdefaults IS NOT NULL;
  oid | proname 
 -----+---------
 (0 rows)
@@ -1654,7 +1654,7 @@ WHERE proisagg AND proargdefaults IS NOT NULL;
 -- that is not subject to the misplaced ORDER BY issue).
 SELECT p.oid, proname
 FROM pg_proc AS p JOIN pg_aggregate AS a ON a.aggfnoid = p.oid
-WHERE proisagg AND provariadic != 0 AND a.aggkind = 'n';
+WHERE prokind = 'a' AND provariadic != 0 AND a.aggkind = 'n';
  oid | proname 
 -----+---------
 (0 rows)
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f1c1b44d6f..fea84a5415 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1521,9 +1521,11 @@ UNION ALL
  SELECT l.objoid,
     l.classoid,
     l.objsubid,
-        CASE
-            WHEN (pro.proisagg = true) THEN 'aggregate'::text
-            WHEN (pro.proisagg = false) THEN 'function'::text
+        CASE pro.prokind
+            WHEN 'a'::"char" THEN 'aggregate'::text
+            WHEN 'f'::"char" THEN 'function'::text
+            WHEN 'p'::"char" THEN 'procedure'::text
+            WHEN 'w'::"char" THEN 'window'::text
             ELSE NULL::text
         END AS objtype,
     pro.pronamespace AS objnamespace,
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index 311812e351..eb61a69a82 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -81,7 +81,7 @@ CREATE AGGREGATE alt_agg2 (
 
 RESET SESSION AUTHORIZATION;
 
-SELECT n.nspname, proname, prorettype::regtype, proisagg, a.rolname
+SELECT n.nspname, proname, prorettype::regtype, prokind = 'a' AS proisagg, a.rolname
   FROM pg_proc p, pg_namespace n, pg_authid a
   WHERE p.pronamespace = n.oid AND p.proowner = a.oid
     AND n.nspname IN ('alt_nsp1', 'alt_nsp2')
diff --git a/src/test/regress/sql/opr_sanity.sql b/src/test/regress/sql/opr_sanity.sql
index e8fdf8454d..590257390a 100644
--- a/src/test/regress/sql/opr_sanity.sql
+++ b/src/test/regress/sql/opr_sanity.sql
@@ -90,10 +90,10 @@
 FROM pg_proc as p1
 WHERE prosrc IS NULL OR prosrc = '' OR prosrc = '-';
 
--- proiswindow shouldn't be set together with proisagg or proretset
+-- proretset should only be set for normal functions
 SELECT p1.oid, p1.proname
 FROM pg_proc AS p1
-WHERE proiswindow AND (proisagg OR proretset);
+WHERE proretset AND prokind != 'f';
 
 -- currently, no built-in functions should be SECURITY DEFINER;
 -- this might change in future, but there will probably never be many.
@@ -140,9 +140,9 @@
 WHERE p1.oid < p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    (p1.proisagg = false OR p2.proisagg = false) AND
+    (p1.prokind != 'a' OR p2.prokind != 'a') AND
     (p1.prolang != p2.prolang OR
-     p1.proisagg != p2.proisagg OR
+     p1.prokind != p2.prokind OR
      p1.prosecdef != p2.prosecdef OR
      p1.proleakproof != p2.proleakproof OR
      p1.proisstrict != p2.proisstrict OR
@@ -166,7 +166,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.prorettype < p2.prorettype)
@@ -177,7 +177,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.proargtypes[0] < p2.proargtypes[0])
@@ -188,7 +188,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.proargtypes[1] < p2.proargtypes[1])
@@ -199,7 +199,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[2] < p2.proargtypes[2])
 ORDER BY 1, 2;
 
@@ -208,7 +208,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[3] < p2.proargtypes[3])
 ORDER BY 1, 2;
 
@@ -217,7 +217,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[4] < p2.proargtypes[4])
 ORDER BY 1, 2;
 
@@ -226,7 +226,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[5] < p2.proargtypes[5])
 ORDER BY 1, 2;
 
@@ -235,7 +235,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[6] < p2.proargtypes[6])
 ORDER BY 1, 2;
 
@@ -244,7 +244,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[7] < p2.proargtypes[7])
 ORDER BY 1, 2;
 
@@ -804,13 +804,13 @@
 SELECT a.aggfnoid::oid, p.proname
 FROM pg_aggregate as a, pg_proc as p
 WHERE a.aggfnoid = p.oid AND
-    (NOT p.proisagg OR p.proretset OR p.pronargs < a.aggnumdirectargs);
+    (p.prokind != 'a' OR p.proretset OR p.pronargs < a.aggnumdirectargs);
 
 -- Make sure there are no proisagg pg_proc entries without matches.
 
 SELECT oid, proname
 FROM pg_proc as p
-WHERE p.proisagg AND
+WHERE p.prokind = 'a' AND
     NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid);
 
 -- If there is no finalfn then the output type must be the transtype.
@@ -1089,7 +1089,7 @@
 SELECT p1.oid::regprocedure, p2.oid::regprocedure
 FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid < p2.oid AND p1.proname = p2.proname AND
-    p1.proisagg AND p2.proisagg AND
+    p1.prokind = 'a' AND p2.prokind = 'a' AND
     array_dims(p1.proargtypes) != array_dims(p2.proargtypes)
 ORDER BY 1;
 
@@ -1097,7 +1097,7 @@
 
 SELECT oid, proname
 FROM pg_proc AS p
-WHERE proisagg AND proargdefaults IS NOT NULL;
+WHERE prokind = 'a' AND proargdefaults IS NOT NULL;
 
 -- For the same reason, we avoid creating built-in variadic aggregates, except
 -- that variadic ordered-set aggregates are OK (since they have special syntax
@@ -1105,7 +1105,7 @@
 
 SELECT p.oid, proname
 FROM pg_proc AS p JOIN pg_aggregate AS a ON a.aggfnoid = p.oid
-WHERE proisagg AND provariadic != 0 AND a.aggkind = 'n';
+WHERE prokind = 'a' AND provariadic != 0 AND a.aggkind = 'n';
 
 
 -- **************** pg_opfamily ****************

base-commit: e35dba475a440f73dccf9ed1fd61e3abc6ee61db
-- 
2.15.1

0002-pg_proc.h-updates.patch.gzapplication/x-gzip; name=0002-pg_proc.h-updates.patch.gzDownload
�y2TZ0002-pg_proc.h-updates.patch�����Hr��s�_k3u����IpV��J�����F��������d$�$�������~���d����<��t��U�Y����C�x3�m��b�L�x6��&�x�yI���f�F�x��+��'q�e0��F�?�f����_����U���ESe�&�����Z�������h���������wA��Y!� L�������|6��������M�?��w����G�����[�r����t��o��������u��1/6��V����l_�~>���E�?X�����o�0x��"��f�NaFiyQ�����������m�^�����o���K���.o����L��������O��E,W�Y����u�Z&I-3��w�D5���od�����
��I�n�f�������������� {-[��~����&�&��/���?�$U�>X����Q���?�����wFo�o������wA�\���xz
�����~�������H~�L���������?��Q�����~����a�����O���8[�������w�����V��]�q�Z�T��4�6\8�J�*1>>:'e&���,�H��21>>�����g�[��5M���6�
WNF�4S����2!:66��;R�Y����Ubx|p.�����Bu:j�+���P���"�*�&W���Mbx|p.�Lk^4�&��w#�$F�����U�J��(t��m���sPe2�X?��l��HT�`�wr0�8�)]��C+s kr�&K!��tQp�b'�c6��6�����(2�P�5v������sQ���*�S;O�����M������D9]T:�Z��@� �6
C�"�Wk���4�����'�P��`��d����)��P~Z-	��]oLjy�:o���*i:�E��h9��	����u��`�{,t����$�ogd�n�ccsq���$������sQ�^l7��b��v��b�"sQ����M�G�mr#T#F������^\�Y#�G�	�F����E�a��5���"X#H����H�l������b�"sQb��>`�n;��5btll.��K�2��G���#1�BaJ^F�����{����m�d�tQ��Km��
�9
L����n�����-;������ux�x�/���S�T2���x/�^�R�V�V����#"^�:/���#�CG�b���S��f<��E�G��k����jo����v�@�K�e����Px�t��U�Ru�
��Zn9�f�Bx�1�����E��w��{�������=��`���Q�)
��I��C{;T{�n� $<!)�M�'�l���s��V8$����mpP��[����3z��!�;g�n�Cbw���
�D��q���9�v+�s��V8$f����p@����r	�x�3Z��
��cu�sF���4@���[5����Q���9ct+�sF�V8$Bg��/���*IqM��/�N�����9i5iJM�iac#�������zJi�zD;0Yj*���N|�>�]��@�q^��n�u[c�qJ���~1oU�A�'���8�$�������>�S��	�p����y�X�|�0;%<p����6���N�
�!��H�C��u���<�h7��T�w!�J!�:1@F<��&����G�� �^R�$u^Bgxf������������qH����a���]7��P���?���;��n���M���"P�8����~�)����U����<�i�����~�R�����@<%����,L�Db/�
Om���|��;u�����4ND�|_������Q���D�%	R8������f��}���6�o��/��;���	*�?[�BcBA���N�!OZ�a�>(;�WB���F��b/��>��[P�Hn�{A%�Y#�AyU��U=J��J�sg���H�p:��_�5�!z���d{�����OF"�������t���I-"�fM�J]N�Y2h`���v�>����:������ln��K��=���v��sh��{
e�J�����������_�w
j���~�ou<M-�N����.Ec=E���7{�3��t<|�N�;�B��ou"8�Y�Sq�Y��2
����x]B���{q<rA;���#tL���%f����������a�����Y�3��j<q|�^��z�D��oN{`/�_b�3���T�\����r��p��@�E�dEt8���1�2�$G���8%/�C��P�'���S��%���H�T��bO#1XX�S�2��!�s���:{.?�5)kn1������A�4�u��>�}��j'
�{\��:���:��������(g�f��Q�R��*$��sq���K.�(�b�v#����E�:J��bG2R�
�V�P�����J�Y�������L�(!'��BuLr��W�uByUF�N����E�Q��u:&��R@u���|.
��`�P3Q�!����p������.3�X0M���pk!��H���>��!v������in+h�;)y�,SF���I����Q�:@z>=���J�=�
�Y���BC����D�z�Lb�+6`]�%����/����r�g��^�}N:���&�����%�����J���D�y�\��3q�������-;<�����N��O6Q4�"�i9��iB�	'�D��=�����p���Y1�(�3F��a1g�f��"4������g���
��8c3+��GfVL82�����p\��Y1���=&�b�1oDf%�"2�x�
�c������Xc1+"���������F~�)���k��w���E���������\��pl����d�^��[N'��A_�#Fk^J!�-��u�Lbu����t�L#*Tl����F����U����xP��xi_G�G�����^?]I���@��}yZ�>������y*64P�������C�����6���'��|��A���5���8��C��7*XX��J�><.���"m���+�N��~�H�w��F�����;""_�_�^>.f��h���C_�.��9"�a�����C�V��CC����$"�a����+���]�nQ��|��`��0~}�
N�a����7�;�C���0����-������)�������B�p�C�14�C8�^��Q>�C����G��b�0��vq�)xp��
^��c�|�$�a�����<�����x��3_c�x��S|�S�7x?"8��|�U���HA�V��/"
��2u���F�i�^Tg����,%d�s�6e�dy������/���h����)G:Mqz:���fcTz�0�JH^���@J��.����L�x]���cPi�ri.�M���M��n]�[�n*!��/���*�����R��SK���:k��-��o�r�_^��u��5�^$�\�c���k������+A����,��1��J�$qp�pX���+�����a�C�78�7�pH�g��I��L���A��iV8$m�3i�
�%m0�lX���
��
+�����a���5�5�tX�c���I��L���!��iV:$M�5I�F%i��hX��
�
+�����a�C�3X�3�tHrkj��I�`M���!��iV:$-�5)�J�$e��dX����������a��18�1�|`2g*��L��L���a�|iV8,
�/	�
�%a0�`X�������~a���/��/�pX�c���K�`L�����iV4 ��1���&]��\X	����+&�p��na%��-��-l��d�VT4��9���HH���faE��Y�H�����'YxH����5���b�#����`�%����^�!�b�6�h�ns�n���>!��/�&������I��
L��w�lsd��7_E���t���P�(#+��R��p�H��������4Mm��L��"&z�'�%&~�H	��m�mG�g�=�����������=1��\��s��PJ�q�m6R�`n�,����~�����N�q�!+$������B�>��� fdE=���������Xq����tAQ��Ub�5"h���s]���T������jh�����3}Y`��H;���+�������Q8]�����[a��)�>X;�wtn_�������e�~���>
��>o���_�9�	B]2�7&�Y�0�1�l���h%��� �vR@����|K�p����I����ed%tQ�
��}�P��O�$�=QaJ�'!a��$���}%�O�F]N�'��8�O
{�)e�$tHO�p�R�>	L��D��C��>	��(�$�'��CVH�q{!+$���}��A����z F�c�C���������yh�d9rR�Myx�]*SN�L��4���u���uwt����h��N�{V$GB��"�z �����j��gEji��B����2w�k����������6�����I0R��?B��Y�Fb@J�KT���tL�����8o[����m+#���}=1�u����S���}�0��u�����ww��5eP�L����f���&�,���6�%���e������:���������X�������+�V!4.�N�v��>d�����^u�����W�Y����~q,���b6Z-�Z�
mJ�uO��2�:���U"@�"h\`}��>%c�;��������������ih{Gk��3;����������{J
H������YqR�{{��6��E�����s!`k��feC,�����$kNU����B��r�����������7�}����*b�"�T��tV���aR����N]e�����1�u*����2~�*�pM�u�IK������1���k2���RHD��s}]�m�<���1q]���h�f�&u�>�S�Te�N2&.�N��:��7r1,���n�b#(a�"J�����X�g�V��h{C�s����>Y�>,�{1�FaFVB�d?S@��. ��c���r@�����.����J��������2��(5?`�n���P�0%/��b�2F}�B<���/��OF�4��p�.M#������F��V����P��B�������u5kG[���f�f+�����2��g��'��[��� P�8(7��z��J��h���3[7��$�E���+'x�"v���Xh�B��i��������}� �`mc�)�5���Oj���1�:��8��L� Y�p�*Q4�VeDd*�D>|��&��������l �9C�f��\��>���(�+B#s�K����
>��[������M����}V��X��z�V�y+�b�K_����[)��5u����� �LG��1S�c|5���q�����a��)�1nn�������1�oDQ�8�K�2>�t�dJ��8G�g�@��vC�����A-t~�����Or/��{��~q(Mkgu3�a��W�a���7����a�R��s���u�����N���v}:�[i����%5��n�
����I��$�j� EovF��Kf+#�df_0[�3�r���-���VFl���T�2bKe����[(�/����2�}�le���Kd#�Df_ [�2�����-���VF�������
/���VLxa��,�b��b?�b+,�(��$���Kb?b+,� ��������~�VX|1�g)l��,���VR|!���^3��/A��d��>��|�fo�o�?����9�z\K���q��~{�^�yS	�7u�=Uy�3��2]i�������~����
~��bYh��$�@Jn��/������t����`Q�k�|��.t=����n��1��$Fu���;wrn0�)G5���;��_%xa�ttW��}aP��o:*�O��s���i4�xju_�����tT�v?W��R �/�JGu�h7t�@q��%��E���s1��G�Oc�~�>���{h�)>�7^�r@e����&������t|[1qeD���F�5mf���?�$�}��M>����������:���w�r@M����F����8���2�#{4�PG6���qTS�:�QFxd�J�SG6��������I�Ln�m�2\���q����w�k�����e_��0��-������n���VHtm����B�+[�u�^���j�����{Mk�D���+Z���V���Y+!���^�Z	��,�Z�F��e�W�VBh%����B�X�U�}4c�X�5��Z�r�`���
�{�j%�����W+!�z�^�Z	��+��ub4c+W�uk�M��_*���X~�� ���������@��F4��W������h����g�E�#��%��xD��v4D	�Z��dL�K����x6^�<�!
)��)��9�lIC���F��M0%<��1�/]�c|tG'��L���p6���Kt���S>���u�|��,����Y�q��7r�\K\�����	����u+#6����VFlVg�������>�[��}>�2b�9�lne�fs��������3������q+#6����6Fpg��m���{������tx6W��GQ�$���/�l�^m���M����dJv�������^�;ec"��;��M�)��QH@�=��>�����l��C6���M_���O��'y�!c����EQMj4i�)��Hr�T��=#c��h���!q�x����O�u���^�������u�[z����1q��=hz���U�I����v�Z�5�"Zko�1l������o�WU�+��S�m6����h^�cy�!�^T�zqe������i?���x�V�<V�V�(u�szX`��x��������������<_���k
��GB@}c�������I�A�N���5>jcd&�4�>L�xlZS�{�A�p�p�wAR������_��P�"/`	��l�zk����o����12��fE���);�_���!��?�u!>����K��a������mD���x��'���~�[+���,DPo�"��4��^�l'�
��+��u!��Ti���Fm
"�m�<������?G%3�a�%o�#]�NI�����t�#�~��;k����@?=����	��eh��4���A�����9�c����R���)�`���zR�sG�q���RvX����X���7'���7p�"������D�������%d���|Q�d	&���\<�I�h+�Sz�c
~������v�d��g������8o�c��g��zS4�S6��4��3_wc��������u%[R��$X8_�M��Fin�o������}����=�^�8_���RF=e�AT��h'���`�|���:����&=�~��	P8���Jl}�u��.�M6;z����.{T������)D���[!`�:}f�oc$�|~�oE>���2��goE�8{��
�znGo��8zn�
��y~'o��<���q{l�7�=AIt��c*�u��U� !'��B�+�y���2�6�#����0pLl��`J�Y	;��E]�7���Jd���0R��������t<��@�M�_lYdA
'������O�������NyB���5���6��JH�m��d���m't����l;5�����j�3�WG�H�8#+!����TR���>`�`?���6oH�)�,7n���H��aPvL��G�h�����ki����}!C�>QJ�v�G#I������k���������N���Y��Q�o�a�"p�����fr�Q�;��"���q_�-���t2Kz�� 6MY�>�
[.Lb�H�L�q,?*n��f������:q���=������4c��UD��A�V���ed%$��$�6Ef����U�[��u�2�R����t�W��$/e��$�.�-�}�F|j���k�� S5��J��v47�����m_��N������f���k�i/W�@����N�j���������?���7k�Ea�&���4����/���l�m�n��/�aZ����%h�}w�UU�F�,�EU��UC+T:L��y[��L��6�}-v�m���m�jS}��c��3a����F=���#���PU�`�V����T;���t_��u�l�����_	>Bt�C���=bME1�E�[��}YBO����� r9��9�tSd'�LH��Gc���[����kR�s_)���^g���=��N;oOg�@�p�@�kpBn��(4j{]�%�����H�����Lx����,d��>
++hx3��X��dX� �7��b!o�a��������.�C+"Z0�}4B`�#����a{Pa���
���
����b�d�.vKn\����(x��������������!�X�
��B�z��]��*Q��&��$��1���U���J�[����R�������h�O���
W���BX��^����R���f�S��5�Ge��j���L��A�!�NYo�"���z���]��'1B��w��v�l���d�U�5�����v" �xlp�:�SI�I8���D[u�y�43G-��_��tK����ks�����d�n�d'�-�����h>���m�BB�[�X]��ZJ�[���c�s����n��G$�Da���������Ew��Vw���������N�w}�R,ql�$`��@+&!{v����$�zo�Z2��I�d��9q�W�%C��0��I����,��e�%�n��Z	�	cp\hN�;��2G,C����B����b�!G�<3g����}3�
��jcXr���%���������n�1W���{�p�Q�D�v���9��rK��b
g�0�����u�����2/�!����N�[�
��n�1,}�����+Q��Rp}�:��~���U{Qd�u=�6�]����A��et�L]�)t�8.�!��R{cv���(n7�V�X3�Lh�N^J'�^\QS-;�����a�M��F���8�_���>o��"�b�|��H�NfJ'�^^R�����;��Y7��#��}�z�9d�l��x�4r_e�R0��!Y�,;�@���$G��Rf�4h_�No�G�"i]�.�ME��b������WcZ��PBc�vmiH��B�b�qo�����
%�@��vr0&p2S��r:�����K�k��U��=r;����KJyA�t��1��8&/��u��������0{��T���A_���kln���E?9y�0���x������r�m�����������-:���0�wI99��S���P|��R�|������AV��#*��k��MjZTY��y��V�	t\f�[=!�~���`
m�c*{�q��*���#��Fr�~�1�!��D�sR���rr�u����l����V�f�����~p����M�G�t�.BC��p��1M���Mh�� ���|��\��x�6�hO�j�YO7�+�l��������`I�F��tx���!��=�i����o��m��|R�����/N��.��c|�/��
�c���X���T�PO��9?�����%�~F�����x�����{�s���������g�]s�/
��*�*���I��W� :z��oN�N����������<~�A>���A>���v:��R���n����"l+��c{+*��[���b��U��[���A�p��ob��V ��+��`\}X������
��=8WV8d������A��U�Yup�9�p���s���Yq��7�t�z�u�a�CV�k
+��`]iX����:�J��3XW�Q�2x���t����PS��i��i[��:M���E�I��tR�zn�J"���X]m!mJ����D6e��kR��Cv�Zkb��9j���N�r#[����T=�yWO���B�|q���"������<���+�1P� Y����sdrB��d	9�p.Q4���S��b>xE|�P�a"~�3e��������,�G=����12&.��N�=��S���0��
L��+����`���)�;O���oE�)��e�gt�B���*A|]�~o�2�+�����p���i�y_u��M���o#&�R?�C{7_����r��L��������!�sXX��a�9(�p���V8dHp�l�8������	@�*�m�[=>�m(%;c73g�?���������x(��^�f���l�F����U��*+���:@��*S�^�Z�~���'��f��m/�RT��,�J�-c���q]CM	]B��nn06&����$��'���;��I�������bu��\1F�N7i�Ea����s�u�K���.z]����Y�X�g$%�����=z�6]���v�����.k���.~��	&�.�K����'m���nlx��m�q1>�����q���y�[!�Q�<����g�6H|�3�o+$<��G��-�R�%\����j���h�����.����j�����{��}s�*Q4k%�0	��U��X�M����@>q��lA#���Nv�h��zJj8V��2�kv^�z	����9F�Y��1j��.*2�t��fmVR�2Gn;"o���()	�!�������j�Q�:���r�l�fw�|���U4�E����f�4,�������k�v����%w���?��z��+/��q��F1x�8r������g��O������&0�>f�n1L~�3��V��g�� �=����|;{y��AT?���O�u�7'�K�e��e����M����������I�V�
[��0@\��}0������n�G����V)�������V�tc���q�LC��`1s�}+D\��5U�!���b�#n��q=�2T�t*��4�A��S���Wd���m�8��	�h�h9@Z��!����5�V�*�>������4HBAgcK�t�{d��u�@���tp����`M:pO�\�n�W�>l�,�e��Q�����pKs��y�����Ks�x�������
����0x�+ ^�VLBp�#�����C���~�
X��vL8��*������1���a!c@a��
�p�J�����������7����i�wr;oa�!�:/�Xh,4���p�'��>W�}���W���t��h1c}���m'i����f�K���C	& ��p�$�	��"26�y��J#P�K�x�t����o�Z�t��L>\�������0y���K�9p�~zR�q�!�ex��vBVF�	q� +#�����t@�����n�c���au=�]D~V*T���fin<o���g��2*��e��<\a����������z7�
Z��
T&��^'S9
�3�{��r>��J���20f+w�H^��j�fg�n��(���[B�H�[
);��O6�=���GA��x��r-	U9L��y�r���*��qL�?`>�T.�t
+?����<���9����E��$�8���a�(����G�L)}9
�29*����_�v�M�*�N�1�0�q�a{�����P�e����AN��jh��tD�+�V�D`�]_t<����T���h1�����I;G9�ohr�u���)�R�m8����3�(�jHze��u���Uf��/<���[��y#��qU������k��W�A�R�i�����"S��[�za��
��Q�9u����������l�b�z�z7����xh$��VG2<�3p�R{bv1�����d��������4�^��!�]�L��OdjO��1drru��16G�v[4y"\M���\=G+S�����P.�&{���L���?zq,�\�������d��a~�d~�N��hNUQK$;�E�����!���tA�W�x	K�������M'R�Y�c�{$:������b+>M�����Ny���C/�0:�hK_���]e:�����p_a�j���1�Z�+�T�Q~�U�e�.���b����lB��z�'��.����[�j�w��{��]m��.�m�E��{���F�4/��i���q�BK;��������N�t��i���{�.hz��Di�Z!�W����TlA�E��B��R��E��B������2o�S������N�i]|:���Q�\���YU~�J����hQP�C���P��y�c�aq���v����B�ZsBa#t�%C����KU��l�o�q[��l#p9s�\��F(�EDl_�CO���*��(�F�e�����t uu��t���Z$�������e���N�]�G�v����47�L�89o��zi�wL;��Z��;��	��c�#}2?k��5ss��ph�s�:���t8`/U!���uC����8���/w�V2�G�No_+�1�D�]Xls%��������2�����v�b�58j[)^��&xm���F����9����N���N���[�*��o������Y���n�H5FI�! ��:�|:�d�GQ���.� d�uY�L�~��}��c�=����+��M.h����r���Gr���J�]$Z���0��Z��r�5�p���nkI�K��K^��y��D��C{D��*�B��|@�?}P��/ee�����,/L*���!� �Y��X����%�5|nox`V8�v�K�w��>����@�B����J��B�K���l�� #������"�X�G�B�;2Z!�.1>F:eF�:-O�<jp����x}����N9������"�����}�<��r��P��?�{d�r�TacW�~Nf���InE����ed%<K.��?�I-0MZ�����o��M$�V���e����y��R�?o#>���k`2�@�����VU��n�����&��
\�F��m��JU��q�u������I�M;�6�������8��
S�2"��Ks��Zb�7�OI�&`2CB����������V�j�����b�8%/#d����'2�O2d�r����9�����tG�e��.�^�K�n�t~�tw�g�4.M^^������-��.�@�.�,�h]�4=����G/(HW���VMCG��1o����o�G���n�����n�G���n�����>��9��K�Ss)�u�RS\z�����i�~�7�CJ�Y���L6c������?��V���q����0W��zP7X}	w����{.�f�1�0;,�|��`>�`�+����7�Z�C�%�$T� !#__��;��NO��oT��ye�>���EQJ
��������6��P=rj��j�U���_�]���0�dDno�Z!�-�=�[U���Lz@����}	8����?.�%�b���.�p9h� ��G1���!c�u����:~�x��e�v<d���];2vYG���W�"%j��\��N
��V"9�I� �GN-�Q-��p�M���q��A/6u��,�$����J�����!���|P���M����~��[��A����Y�=g�����;�:9�������% ���k�W�=tOO,��C��_N��w�:���T�g�>��=]4H��N��b�4��X���@|�S��E���4�8^u�i�E�N&zC,r�;�{�>�~7Fp]����]�=�"�
7���l�N��(���1F;��������v�Pc��nB�u��:��^i�n'�]\j7���%�	Cq������	��kN�bu��s={�
���{�e$��`��#W��Iy�8��i��
�o������#�����N�,m�F��M�K��-������~��C
#�{����y����x%�0��d�������2��!��x�Q��d���j�4Y>�q��!����@�~�!�	@�B�<q��a�j���T=�S���u��U��H�TkT;,e��a�j7�
����KY�zX��a)�SkS;,em�ae��\�2�K8_��i�u)����Y�2�I�����yEj�V���Q; �e^����(�Z���E�W�v@l%���b�P�U����
������>V�vXt���D�F�c=-?�p4�	���T~P���
W��������r��\�dC�����>���F�����YI�>j������ve��J��B���<M���~z�Jd�9i�%�B����C,Y��(�8D�;�����#S��[I�y���C�vXL��
�L�3�����%��R����G�x��#!)��F��u�9R�q��l����B=5c|�t���v��=�^��E�#�E���zG��W�������U23���58��=R
�u��S���i�
,VIHV%\�W%��fQ��]v�}R����p�Mh+����������|��cDs7E���OY�'AR�2����>����]�J�������\t��a������M��8�^8>��s5��i�5�`���<
������n�T�Ur0����������)�5y��n�e�|�/^�^v��k)�~ABN>�z�3��mk�W"s�`;��H��[�����Y���U�L���J����zGa�v����r@�b���V�g&5�h����gD��8�v4�'3�,P� '?�U������M����	��Y{������D`�A:���;��s�|?i����nh�L@���8H��g��3S~d���x��Z�\�`�@*�Y	q%�L�����; KJh�#�U�]��i�����6�oOm�=3
��xS���r8�uw�4�I�J���&�� �SP�A;���w���G-Y��f�l���ykp�'������3��WH�)��*���Fdh?�I	ZA��A�@���o=�	��{B]��^k�n{���F6���v�j	T]� <>��ZT�����+7����J�~��	����!������T7D���L�g��FKP�������{Q�����Xtk���	�~�-�����>��p�������^��]���Q���nmLe��	���Y��������������5/D�j��AP[����Jw��:A d4@�THg5>u�@�x��?� �T��mg��yp�06T�(�����AbE	���(����b���}�����F�H�����Y�>��V�E#��k��6�c��S�
~��������M�$��/�6g&�C8]A'7���s�i��`�k�[�VJD���,V~R�L�/������y��t-�'��I7�����J�v���w�x�Lm��J�Y��j5������P����&+6�{�g'0$��;��������M=i7E���*=(�J�(�S����O�/Af����)�L6�uz�SI-�x�����AF��t����Tl��k�%�*�u/3�j}(���+��Z�+?��:�O�C����I��k�m���"��+B� p
��B2#���������P�p�6�b��7�J��K5��B	�s\�+p��]��~J������u��
J��Q����u�/J�<��TB�����R��X�)�����n����N��Ur�J}�4�����e�������h�1��O�0^=���(��;DP-�����y��2��%�TieD�J�����N������&�'I+#<I�N�VFp��� ����==�(���}r�bR&G/S����'�������h>
QmN��My�}-v���@���7eQ7�	�?�8�P^�Z5zV�V��t"�[���+S5Q�v���3ne�.�aQ���=f�����&� ��,v1���HNI�H� �r�S*��7��pu�����i�]zY4�+xIG�0�U�J���x4z%
|��
K����C��H����I����J�w��>*I�f�&Ux�(U�z�&?|�k��n���_�����(JU5(��[��I���l�_���M��LM�� ��<���x-�E�����i[�f��a���a��L�������>_K<�����ixR�������g�^[f�}��~��������vL���)~�rA)3!��'0���':}���������y���dfq���B7�zQT�����x:n�	������k�U�h�Q���������CU��z����W��m�:^���$,��Ft�P}��HG�P����^��-B�/�D!��\�u�~�W��0
.���h.��pG�@��5��W'A����W����2�U�������o�p���������Y��`�S��g�����;����J���/���;�Tq3
����t������]�����V?�����_R���\[4ee~����?�{������x��k�7��^��R!'6�f~���|#��� ��L�����N��>�"x~�B��2]q������1�pf��>UyN����h}�}�Dfmu-[a�CFI=p�D�����.�b��b���e
�rP��������x@�C�PneWa��S_-���!��������OO�:��W����k��A��	�������H/?d�)�������e�A����#p�
�+�A3������#���qy�KA�� ���"�yq���\��J�n�V�;���BKG��(.��-�d5`��Q0��F�l>a��K�0�|y���#QL�L���F���	����p�-�s`��d��b��novE_�u'v}��Y{��&~�d�=�_�B�~��+�!A�������O��HvH�#��#�+	�?������eCF+�uw��/�J�����;�-G���gR��R�s��7sQ���/�l�~��O]��jD��e����Q���R ��"�;�po��S��'-��y��W#����w��)�������~�g'6E�9����b+���������E�g*�/d�KS���y��_/:����0G���AM���H�i�|Xe	����6�
�o��=}�cV��N���_~?�:P�gkJ�i���0BN>��xs�[���2�3u��;1�JAFVB'�.z`�Ne4�QiN��g�v>$�Mi���g�V8$�_eR_��n����4�����4	oP�#��Y��$	oO����Y�2��Wq�bwAvD��; ;"�����q?����9v�cGD\���#�������2a<8%?�2W���x��N�������lx����L�`���*�>u[6���)7��$5.�!�� ��x�a 
����l>@��$���P-=D!�00�-{Hz�R�a 6��G���c����m��mGDF6���#"��}T��Q�=��+���E��O��m�BH�!�za�^����k_��9��:��-�J=L�u�!��s��@!V��=L9�����N��t�SJB1�`{d>����V8�@�d6L;"h��fi����(���Q����[�0b�H"���fd%t9�3[u��a�'�#����3)�^	�������9���S7�b@������m�dXM�_a�!����8��jNjn�j��l�]����0
�U��V]>�W�n}z���
q�������
qW#�m��n+`��a�vX����[;,�n}X��`�>lv���������X����4!�#���)���]V�L��]KU\��Q*��
�X�5�����R�B
S�� ��Yjm�2�R�*�'h`m2�.���������m�y|�[��{�yr��o����!���G��I"��%��#.��1������?\���+��w����0j�x������t=R��������~�����^�����>��������������������������o�����c�y�uP���,3�:��������Y��J���,��3�z����,����0zY�Z���
_m�W������&����{hx�k� ���=����u��Z�g��*��8��E@eu������\�� 5q=�"�Z�uX7��D�:'�����u���P^��7��c��Z��J���
�b-��C}��
oa��D��=���Z,�XwY��������
�;W�����\��z�������@����=(A�G��0\�G���'-�)�%�f�7�u|��vv`�r�/���%8wy�������e�������Y�N
�Z^�,;)8gy�������e������le��g+/s������TvVt��4Ou+/��������6x���hS�c���o��g��4���D-��aV�7:���i�_�l$��u;?�=�:���&��D{���}�������|��`���N�=�%0����7)��-C������m��(��+�H����bw}t����������V���$x-OU�������?�fo��iz�D<3{���,�������|�"#�����Lp��=2�zu����Yc�G=m��EbDa�G<�=�!t��r����n�0����{!#�MV7�}��0�!�U�}���������cp���FY�?�9�Q���=�llz9s�E%���k���������I�����x��7i	�MT<��R�Sf��A}�t��t�AQ?��MxcEtS���������M �Q��i����}�e���W�1�����R��}�����E�����2�!,e��m�6�r����;���?��g�RV"���J���&�:M�C�_����p>6���8��$���%Wq�1W����^�R;����h�
h��>�����@��v[�L�����:�{��H�b�&���6|�2�d|JY!vA'�g	������t��n��<`��
����}��e@��K@�������PW5��	}��O�=b�e(WWG��[B����*��5V�uu�=�U6�M�z�����2:��B����:�6pdo�����U'I��C��h�>v'����Zom��![+!�?���}�����DV���mD3}�Ft���x����#�aHwQ�=[<C���y
6��6�;���6��Vi������i�&C�e�C~q��� 9�6y}s#
u�X>�q�����j�����%�f7������
z�5g�����&k�}�������?�J������9�U���f�����w�w���|��d�-2�/2�j�_��iP����n���o�����LUV���Z��^��N\4���W(����)P�&]Phj-�����q�rt�_�����/~����+�3s��_M����`����(��u6����Ro�����Z������s��������Y����V��_;��@b�o��N�<_e�:���Z"��P`o�C�-?�3Xo����2�{�eG����N�SR��G��DvR���~X�zf��l�������Is�4���L����o'�=F����Z~�7�����;��n�:�l���q���0��:������g�o��O�Z�p|�$H�Rv�+�}������9����jV�N��y92O�o%��
��s^�9��0��=�4{�����zg���mbf�{gG��3	wH�{	���z����]����^�8�)���M�I���\:�����BQ��C����L�d;���V]?�+Z?(2�����#z�a�#��$�5�`%������ �+���I	��n�VR���[���`��v:�Y�]���b�H]��fc��*�D:�2��Ul�����4gg�4���	�3�5���gG��}i�n|D�Ep��q'S�bG����R�^�Vw���d;������B��e/m�O��)D�x��0z'�,!���r���r�U{����!�h�oS%G��x!�������= ��w=�-���
��_�&���D)E�r���h�1� WBP�UH�C��t6�$��0����j��m;�A��7�=�s���@Z��iH�� h�p5�="BZb�&�Nu��GQ�(��Q=b�����*�z@�b����x$�!H���*I{D�b��G�n[{�2�:������:iS��I8{`������^CW;�r�v@0p�[��X��������1d���!+g�jV�p�"�2�v<,XeU�x`���Z�@�3L�ba*c�j�C�T�������P9�S���S���R�!Uy�������w�x\�4���$�6e����Ev�T��:az#3f�R}:�#����S���f}:�
�z�T���x��*��HER�:���`�+>O{�@k��t�A���~]�~�:8R��.���:2�)�m�f�z���_o�����@�4�A�t|��s��U��QN�mhN�i.��47�V����{|�h�Q�v4H�>48�BJ�U5�m���q?��M�j�D$c����Ob�*�P������(��-�*1>d�`����3�k�����$\h����%\��l>�9�[�Vs���%����|ts8���1��I�0d���
`�2W;��pe�v4���8T�h�C�q������9L��W���/�l�~W	9W�W���A�]A���.~�gd%D��H��A��p��r��@����#�����D�"W�q�|�TW���_E����V�;t_1����gAFW��f*��9��eG���6j5~�~O�]'���~uP��:����(C���p��i�vd����\;8�r}���[��wu�/n�=��D����tn�j.���FB���]K�FS���gN��$T�4b����?|��0�������?�Vt!�=N��y�s�`��q��x����7������:�7s�\��=�d���t�1�wJKz���<��q�?���*7��k���y���/z�)���k���(��)��RR�,7��e����[s{�������l��H�_���B����w�)�X4>�X+�'�����R_����������Q5��������Hr���YKm%'s��u�Kpq4���@�����N[�}c������V���7^Fc��E�,����^��-���(�g���X�E�=��$��+k�/��/�GZ����l�7�ki����{$�97����{n������75���k����|�@�f�HLC+|]�H]���c6����������s�������}-���^�yG��oa���}-�!�fa��C2W������[��A��.RGM�o��C���D�i�w?���`����S��m}�A��Z�KI��qV~��J�V��k>���r����F�>:Q����>pG�������4B��um3���E�j'{�u����y+��V�{hz-�"U8�����N7�m��+�2L�$�y�+<�d���a}�R����'���x����yN.����J��g�M�����+\'��+-A���`���O�����a�"�XaLn���Y����|��fA�']o����F��`�����$�O���*e���W���>�G2c����,:@V�=�*�Q}��T@�q*o������jj`���
��������0��qZ/��5ER��r�@��_�;B�@UBplh��|�j!C�@~<����09��.�sh��R�kR������D�����)���S�nG��L��W��b��O-U(���'[���(��J��n}wc�dj���#YuK���cC��e���V��Q����sjUI@�
�q�����_��[���������<s�RI@U	�q�
�����0��=F#\�/���b#�p���,D���J[���"m�����O����UslI~�w�K����1a]�N�F��w���?Q�M�e���y�w��������+u`k+��P���]P��Fy|����%��A���I����.�F����I��*�:���F����_���(�yA)��|��Fd�_�/�q����^^P��}6�X@��q�\�����������O��'S���-������x�av���2�����n�3��������_V��Q���r���������?qPf���U�����j���0��������p���Y8�����X1%�BK{f�A��bn�k�L�|q�,�f'<�d�t�	G�}Ng� �.�����2��/�N��A5�������ZU���a��������T�T��^������j��
���1�?����$�b��
VL���1����bJ�Q��.�O$��IF�������w������o������g������_������W������O��H�\���"�j����"�����8T���$���]��H��:xb�b��GK-1{��n���Y.gX��MV7��_��X}�����{���m6��Y$��_u�.�!{��w��>]h�������-y����r(�z^}=����d_
" !Gu����S���u�t�x�P���;Q��AN~J[�z�������e�Lg+__�5��Uy
sj)?��}���J�1Y!;=�O����3����qTv�)]Oy�~��nK��Ks�:�=��*�������������h�'�S*��x��N����}5��1]*^�:������@��3�t����._��Y=�0z�BlnVG]��w����tR���������L���t�b�����T��Ft��1����f�6~�-�5��&;����;5��,�<}���q���3�J���`�������J�o�pB���B	�
����P�]
���o��>W��;|~[y�Fw��,����9�)o(����&3�K�;�1���X����I�f����`m����G�n}AZ����OrV_��_^����h���fo��dZ������h^k���Md7f��W�@�m8��pI��e��B�:]������=�R�y��y#��B[�iVx����{d�%�����zf��d�zT���4*�CR1��B�����^�W���}(�@�5��Oy������y�b�g��L���3�;E��,g3��)NQ�����������t4m�:`�T�+�z3��F��/�&�bP�8�:�����4^i���`T_PK����G��
�q��Z]P�9�/��(���Pm.����p�BSmh\`�6S�6�"������������9��/�����[7��������p,�����j��f��`���8�.�r�J�}�Un��}���^�~U����X�2�b��*���T�����mz5��W���)��U2��Y�7W��m��@5
S�2^��j>����&���663S6����B�n���tB��P6����e�&eL�����\=^]�+4��orRw�\Q��a��P/�����zw��)�XV��D)����b7m�s����9��n%��r
���`������Mu�Z��Fm i)�����#������R`k=���0�!;�w<�� ���BL�N��i�	�e��I���E�Z�l��������������U*S���P������}_����j#���W�i?9��v��(���]�A� r�z���6�������$��s8���P� %7��s��nHv�4�A�a�����%O�9z�Gi �ulA^�#I}p:;�0~����#F�AG�<��!t�#�(����qG��1J���8�������]���B������m�j���P/�Q�������\�rc!��U@�v�@�Wm��T!����������e�HGC��'@�����qq�����^KI���	��.�l�QZ��Mz�(j���[��c�V�R��`'�;�����PJA	���p�{ �RP�yG��b'�y#���LY�����-/���"���$��O��iI+.�LE����{���6����o�mp�(I�=�3Y(��&�M�
b�V_X�w��A��W�����q�	����\��B����*c�����Vy���q��.����T^�j�C*�;������9S{pg����i)���#����9Rn7:l5����d�{����Q�3�����w�W��Z�z�Q����(F�[)�nQLfH�0x�6���u���k<�Fm)<�����)��w��:���5����WQ�(��Y����U�l����Y�c43��L��c=�t�'r��X)�`�|�t�JYL=1=$3�a��S���8��rL�S�:�/a�!����q�cFv)7wA(���!�P#M{Pk�5�P�U*�n?�F:��q�<����(�CvLp�� ;&8����?��g"X2�Q�o���UQ.���:m��j��y������M�f��MN���j�H��g�&���\m�&�n Q�/����i�+%��!IJ�k%�S�%�U�1���I;#f��ig�,�����y��I��q�EIA��9o��.�c>��<�Bf�!����������,L�����rG���>|�����;�����8�n(��fI����F����L�����Bx��R���u<�����zT�����^�����|���+xb*3W��Yx9��e��|�T�[����wob.�m�L�#�_z
&'��F�	9!���?���4}����y���@���>P[Q(m2���l+�����������
�k'�]\�V�M�/`-%^��
�\jz=����(����N��4����z�G������,��G/��%����,{�����%���)L]1{/�������b<��?%z�u�8��?�����b<i�<Q��4'��Y�I�p�����'J�f�da�~U"w
L�?,�Vb����-p���R��<���b+�u�0�N�_��1�U��5��\���P���
����NKX�xZ��y��������6��2���+/�++�.��*����k;(�&�_���	�z����G�W#vFd5���3"k����Y���C�����UH_�X}�|i���7�os�6��_Bu�P�B�����>�'�k���^e��T������ rvn?���q���&+6���Zq�Y\Z��5�;�805B�xM;�(������t���Fj�x���y;-��_������n-C�����)����b���  q�f� /����{J�S������k-\��Sk��������l�Sk�mo9�����WZ7��V.PZG��JA����f�:hAx��|�Z��^��o)����B�"�Nf+B��Q.(�����B��k�v>���2S;1�L���1RN�������y����\=V�����
���n"��N@��pb`��{1w:V�K��-�MCS���������D����R�?��P-����W�����T/�t�lJ�^LN�I�������)>i2z�!�8c����RR�pB8�\�n���C8�']�z����6���]��NVBPH��Y�{Y��=����*J��I��AWcF���u��H�C]��g���v>w����|�^��'���}"�G���{Dnh��!�7�#B����	1_��	'�Ds��.��nSJtOmE�Z����ClfwA��e�����pa�{'��a@N<'uF=�����%�;X���Y�U���w�+���F�m��:H������+��Gkn�i�p�R���V�i�MF.K���l3��[I���Qk�����w��P�EO�U��e�]�_�%Wv����jn����z��uF���{�`�a��A���e��Y���c��Q���=�������vL|��zS,["��s9\���$�dU���R�-�����M� /����&�=���E ����kn��?�e��M� /�p�A���b8�w��o0i�C?��|��P�!@^<��|v��*)+OR*��(5yP�@.I�+��Ji��G�H�(E��:�<x�R�n��i��6�ve��` 3�����-����6������2���T����
(6��!>}c�O��-7��B�I�a�_�{:[-�����w2���o_��d�~������8��a�;+7S����:��o�wb�f�?�
��7V[(����H
��l�����*F�	^P���{�M������~���q��Wf�2����j���r�Lta�p��N�Q�q���|zR����V�}h�R@�����v
O��v��g4@�RG�y������QBPg�N���i��5uU����6������Ff�e���U�A� +^�L.�~%P�u����3���!�*Q����8��f=d�XHf�����q��������o�����m�r*6���Z������59���~���4����c;��u����0B^��m���$�;9�T ;M����S��SU�*��@Q0B��w�`��(��X�Ik��p4j�������h`d'����o�xi�P����H��d,FL����9�i]d"���Y�
�&kNS;C��=f�[w_�srt���3�~m��dKe���h�.�
��.���.W���?�E����U��-yw���p:t��}�W^l*���������G5!��d2\X�����m�dk���T���VeO�#�����+�;<�.x)��T���-��2��A-1���&3�|0��gu�j��.���{���/����'�� ���"�}F��b/�)1R�.���(A�e�}%�� �;� $3��v�,���F�
�s1�n)�����hxI���kk�B�i+?)�Ejjk�/����J�E�V=�57���N'��Cf�W��ZHanr�']��O�W�+�����<���������r(ssC>�9�'���G��t�wv0���N��^�������0�;��kav,v:g���V�t�n��������R�Xf/���\ff���Jd��n�4�$U�����CX���snU%�����������s����a�Z4�[wU��m�*����hE��D���{��^��^�L[��i�['��gd��Z�7�G�\��U0m#d\\����I�U��
uS��J�P%��qq��(?wS�:������T��d\\#5���D�L�1�h#0��v����#T�+��N���2����?
iW����&�i:s1Q�s���n��3�K��t�b��*������$�M:s1Qu���!M��=��v5������:FKs�]EP[���166����]�r5)�fIu��F�T��U,���>�`�����`c#�Aw�����:���,��w7�t���
�x-��f�������*m���J�u'��:�>��l�=^eR�;�E(�������M����N��u���sjoSs������i��c_|��f���*�W���~�Q��W����?��v��k���^CS�&�?������tFb�4������E�e8��	����������Pm��^P'4��_69b�]J��10���0���"/yU7��5��0�ZO&��I���V�N�3����u��@�������M�sZ�z;�G���{��$m$�������]��&kDP7U^��"h����)�������j-�Mv�{����!n��\�k*/��E��/�_�d����*�#�����������������H�eH����K��9NM��ON
=�����vB�f������>�z,{�D��D��2(O�����6yuig�u���)���M=�o�q|��Fj���Xx���K;����Q�Xe�&#M�($I����F7�R�/�K�z)��F������y���Q�;f����a1vFp�0����T�S���s����g�k`���Z�F4�i��A�����2���@�5_^��-z�k1O����aQ��V�ZO��^����Z���"s����(^����|�`g%x��o'%���o%�G?���S�c�}���K�L���� ���9���[���>k�6�����������N�6�o�����������9�x�����(��m�uS���0��H��`�0����r�C��OS������N����T�/?��������Q���^!2�����&����%�����U���Q5"d<\���n�12/E�F�4���
@5	�q��K���7�>��D�y
>��Ta�n��=f[�:���3v�SOJ�X���m8����W�l_��u��BmZ�z�������^������^Q���I����	�&J�^�
�O1T�2W��L���FW���U+�V�'���N`�q��+��8647�k{wU�^�����I�R�T@�6�P�#���]7�5`�n*�>���n����Z�X�-�qk\��i�������L�(P��}&��L����������+<A�rm�����w���w�_���?x�F�������h����z�{�/��@ddJ\���LV���G����g�c�x d�t������b�	lR�H��������b]��jlT��Nz���Be�1�3!������b���yU�^�(}NR:8l�I�Ac��':�f"+���C{�����V���t������y4KU�T��3���Ub+f������@9>!?d�}Pn��;����X�	�~�
���tH�����d�Dz&3�kO����TP���)�W����}�U_��2�S�����
1�#j����c]��m���
=�0������(��_��|�O����u��*�aN%�K��u6w�cI����m����z�}���"�h���,�
/���,�%j�� q{�>���w����K��]���s�XX���	���T�����Q�{5�`�m�d�B]����/��>�E��o�7	��������*����1
GZ�������fb3�� ���I�;,z#�7a��C7�j����
�	E;�����=KK���������i���Dl��h���G;��i�B&�EDz�������}���W���������?�*��plh�+r��^�|zZ���C�u���|�[A�"plh����5G'T���G�Y4����wA}:�}_sU��l����Y-�O���S���b���M�\��c��m
�p�X��"
��G������3����������>n�m�����?��Z�Zv{]������}����vk�U���L=i��t$����E�=��IX��9��p����p)�����e����(��!M=3���oZ�b�������!1�H��]���po��F*;	C��@���r�j�B��z�.O��b���3�q[�����0���Y������G��n��\E��-����"}�m���7����C�:�F��H��j�2�S�!B\��tl����0�O���|�C���l��[WR��H�����tE���\�l������y��d����~����8\]6��}Z����j���� >F��}8��)?�A)��M0�i�h�*c�]�Aq���/�����~��
\-�����2�B;���>���Q��ug�Ct��9�����n����\|�7Q^�*�$ss�X��h��i/�����.�[C�����������%�~���zr�hs2�Du��
�:>���Qf�$^��
��R�dr����;�� �<}�S+U?���G����_=��[J�	��������'�{a���7���^��d��*]��t�}i^l�'~���	����I�#�����l'�'�|�5b���7u��G��u�����GG���m0<�g��r��x>�*{�g���r��H�;d`~��4�����%�p.�b��(������*��F{wX��#��vxn�u�
:�\�X������NT>��H6�P��qGG�g]���Qk���U��ABg�B<����2\��$>������Z%��#I��)�����q�
�"���6��A>hr�=�Y�"�<���~9�Sm�s��j���"2�?��s����	�����������t���h����N��>u���0T^T	���l���M�@x6�u�^�72����P�<��������^�J��)������k��m�����!�����/��~��>�4<��=���N��_?y��{��pW�y;
�������+�XK���&�����V�
�P=��M���O
�}����t��,�T)���>�,H,�z�d�(Jt���������h�g��;������w��Q������69n������"4�{,�������{��v-9���e�I4	P�3�=��o]pgXO�Ul�:,��p���deee���<��k� =`�GT��j�K�����~�UO�W3�j=@*FOy��Hq��TI�_���cp���L�J�����!6]<���9�����p�T6��;8��oT��J�}����o����"�_	��x������xtN�.~jw�������]��6K
H{�jP���Q,u=Z�$���A����ab�w����:/:^�h�FP~�O������A��v��?|���?}�����O����qLW��c���|s>����:�d(	�����y�i�
� �4p����s�r�|����'�����.:Du`S37m��|$�&��.J�P]!%���c����^���q-��������	]S��@?��B�����n��{E*��X��Z>���i�x��k�k�����)bYw�s���V9���,s��L�9��2���h���`��N�����.ai���Z#c#�y��w���o�\�74��c�*f�	�px�{C�}����P�Vq1��������Ab��)I�!�B�:�����FT���������?}����~�;�O���������G�b����,���	By!�����,:���6�E���&���RCC��0;,��������[�_������VO�R�"�?7��
��WI���0�HY�Ud�3�mM�-
����	b����o��O���'=3k����K[����M�*32E���Q!d�o2-I /D�.������������^�����~siIBY�!v����\g%��XNl	��NL[�
�#�H����������FY�#�H"4 2nG��ft����:IC9�l�uD�1;Al������������RM���#
����	b�v��F3�*��^��1.�_��b^nV���%��HO!D����A�D&3j������M�j����B��y��Dxpa���#5B����
��F�
���t}IB���)"�V$�X�6j|@��k���Xc9�����r�Q�C���(��D�1F��1v#Lshb��~9���,
�(k��>�,V��C��[�������4{����Qa��k����z��v�Q�:"��EZ��DXB�jt�7�����aVb����`Z&��8/!~�C��C\���j��=���h��p��,����S����Z�p)w[�u��*.�uV^��]�h�!�{�3c�;)�(/���k4G9�[1(~�'.�K5B X��j�@��(��@i1L���a�E�T#������
�H�R�{�.���m����m�>~��r$)�o�&V�p	��g\e���%&�]��F�����[4�D�0
�:�z�n�`T���4�41���I�'���\@#��Oy/M���	����v�xH�RLl��&��y�`�l��^:o�����n�`�P���O8����+���[�x=��p���s�
�r����I��fG�l���J���F�\�B�N��ms`T���|�:�z�F�v���U�6���H�VP%��nV����!�����}hBQy�L��^�p�*��
y���T�H�������-:?~�������x7;i�&����W��G��Bk:Ro��L�/e=�!|EX�]�F��^)� P����'7s���\D���)���*U��4
�[��	CE�s����/���$��3�2����{��z��Ab��vTa�d_��������?��6��2y2S��O
���7�Ol�����8
�lKs#"�hae����A�����
8=������r�}��C���� H�{fUf*K�����,�?@A�U!���U&������7���=���Xzx���{����YR+�cU�(�Z�J��k���Q���()K
)(�U�+�A�O�f�Pb�3W�����,q��W����JG�W���6J��k
Jp�������h`zKA�.xC��@��"�aH�
C���#��� %F(�@J�P�~���c=���U���:� ���J���d���1 �W Mm���x����7�	�f�N��6��
��Q66�[�T*iHk�]����!��j�M�017����k�hN��E��=���o+�X+�:��������q���e�\��QZ�hP��}��V�b�1�p9�i�IE���%-�0d���a���W�ZAe-����&|4	���V�J�k��r�s��y���7�	���y��v8�R�����;�'X�b8�]���	��Ug�'X�b8���"	(Z�B#����F	�%QI��Jb�%�D$5J("9�Gj�P<r��(�hd?�E�Yy0zc��EK�����u�
���;�P�Aw��U��H�������Q�uki��Z�P5H�C��(����S
�N��� S��Jk��0�N��]P{���VGp�@�1��eu����cK���z�V
BMO@�k{y��x������M�7N�������S�u|��YX�����Y�v�����R�&Q&%�N\�E�JhJ�Mt5��i���[?������^�O�6��Rdj�Ep����
V��2��pQ�E���t��9�#q<�9�v��S�E�I��jM��	5���4��� �:�������Q��o�$-%���k�N�OS�RPb�������IB	(���Zf����l	���2Q��&-%��������a��2h
�e�=QX��E$��H��G���#���"%F(��DJ�P$����8d?
��.���TW�����S��������1<��Rjt��u1���������iO����^�u��4�Zvj���iT�`������������/�.T�w�@�Bs:��tf�h��|�����'`KQ`T����	7(�I	lN��
$��4%e�@hJN��m��W�~�tN�@�s������1�^�������DuYJ�,�����>ZK|/f����������0Q7|	�H�#���IdS�C��z(��>����Zx����yuJ�1�>D� :������}Z� ��X� ���������s����uv�����E�?��D�;��
R���;4v_�I���^	'������h`sL�.cc���~&����������~_|�s�U�L@��D�����C �C�L_}�2���K*t�^W5��z���ib��U�f�rysm�R��0ZG����������Y��a�������f}��*z3�.����Hm��9'ET�p1�����W������y�A�������?����>����<����=D��WY�WW6�TNqu��(��(N(+������K�s���5�:�AY�n������]�-�����E�9�e�$Dlq��n�����r/�	1��%2��6�Vv�����_P)_��n����_�	���T�B�q-#�ZZ{����8L�K��G���
��8�X{H"Q������c��������Y-1(T�6������}}�6�\�%5=�E����e�6��	A���kQ������I���{�z^x1H)EX�z�-	 �Y2XL�-!�_`6� e���m��t��7� ��C&�d
��Z��y����\w��lP��o8f��,��8�
N��`�M�J��d�l���cTV�-.����uX��!���i7>����jtT�C������F7�Q
�����%mFE�6ZRTt�Y�,�}��G������>{O9��?t��'����c�]=UJ1��'��D�0���o��v����*�y��-�<�=�w��������(���7;��1���z�h�������|��e�l��&9����Mi��^*�F
�g��3>�S%����}T��o3�]�?�uU;'�r>4A���P6�]�l?�����C$�9�1�Rj|fG�!�/{��)*����TM�(���.�b��*q���P�[}�Q��6�R,��$���c|�z*����������R+T�J$�
�S���B<9&4�2��h$�q��C�:������@Kj����O{��=�{�bV�Oj����"av��"�G���,[�����i�c�q�y(���,;���
�
"p��a*V��$���8�#�Ab��q%`���Q�D(�T%;�z=�(E#Tp��=e�/*��K���(/�c(��6�l-����`z4�����/'u��(�f�S�`��#v�������,������*^��(�4��d���{�%��z������+����=��2�E3��kI����%k���o����|���?YM=���#�� rp�n��,+<�1M���������H	zxjpA�r�<�������?,�8A�b�UBPO4&�&�jh���$V

#�*���h��J��",<��3���4�42@�m����o����d����>��r��P"���FPH��"hW�
����/�k�_KOA]9X�5��4^� �B4�gy�w�}
a�G3��{\��|�
�l3�.KL,����g�T�pd���6�6L3�^]��{8O�
]E#�q2x���<xe��i�c9�x�����?������.,By���4y>(}�7Dl|/lG&��Lqh���e�4�e�X� 58g~9	Z��'�s ��i�0L� ��6X�����1O?�;���2�<���>���=�m��W
��o�`-	�7�����byK�Z�,L��4;�\u!�Ki����W��_
���CG�me[�NS���z����H*�$�s14���lps��_�yQ�H�����h�����mx�����Zn>�X�h���WM
�/����P�,�������5�,���\D�G�,'�p�����\j[s���{�����k��=O����9l���6Bja�����y%26e1�s��[|��D����.��6�h��i��wB��������7�=@_��7�_�Lwg���EvN��Z���t��ky�eqr�A[uR�.���Kk�{������ �M�F��Q��E�,�C��9�1�D���O������o������d�F-
�|��f�kuO�Tl�waj!�j�}�:[�?Wm�j
���p��P��x9	�nwq�
^�/�M{��{���q@�� ������E��Q��]�?��(xS):mugQt%�2�Bv��U�����0���U\��
+z}��ha=�����`e�P��Z�.~q��	C��^����}�M������I�0���~?������{Q5L�����a�}��T
�A���j�`����T#{O'}�)�w:�9�H���I��Fj�o:�5�Q*���p����{(	�-t�J��d����p5��D�^��~`L�����~9���4�m�e��I@��M��m��'1������$��5h^]�s8�4��"f��������2�C;���IS�0���y2y�K���.�0�g]���tfq�|�������N
�|{f��;����������^����c�G�Q\W�7u{�!�y���e����)��������4��ee��;��f���#h����1�o���W���J��aq��J���b�LI�-��-\��`����1��6g�t���X0�E�����]�����qX�S��zBy7���g��R�g��.N����u�h���|��/C+�����T������'��>�,��= ����������`�����tHC��#(�c�{�|���M�
��l������Ai���+#s���GNz}�@K���o���3����s��1��L+?��
�3��>�c�G��^T��ps�TU�K�@��1^3�Jg����@N��\�D�4`u���Z�75|��U[�^���v�W�y��`�E&���1�����������z%��a8�h�$�,��@^�����e�l�*NMv|�*�������H�Yx����-�3>�
z�����s���>
�Hz���Y
z�:��-��@��]�J�5?�K�������'
p�����,��#������	������I~�����i^���5{�^�
��g�cx2
�6���d_��#?O���K���E{U��U2s�D�Cm�Cv�^mU~�_��Q��x���
���9")5>���z�Y~����SI��-��P��H�>�ZuY�j�'��������o+��^-i����]A��
�V>x[��\�W���qrvs��&���,���������;�5�R
����-9\>�����is��g�Rg� ��*���&�Vc�9�����j�qs�7�0s`d��l��(e�
4�9@���F�\�����"N����E��H7���0���j.����_�u�������'6.%�1�jE��v���1.��_�_�XS�}HE�QT����~��i������� �->cq6���H9gT[m�Y�Z�5l��(_��v@41�6�K���x>��&����\�D�2������eX��^�3��Avh��*Sx�R*!':1c�i��������!�����Dno�T|�����f|�h!�%�v�:��3�.-���dwW���i6aBP3.4>�.<���S�J�j��ga�@}+-�#H���S�|f+�+g0`Q�����&������+���<�2��ZB �S@��X[|�O|?~�Y��w��p��?!{_����������r�Z�C��c�Y���������W=@ba~U9�f���^`x��b_HG�������}�4k���NoF�>���7yD6��um
.#|<D����������Z������y�xx���2���[��u�ltO7w�J�mZQ����0���P�^j-�+Kcq)�i,���Fv"��g��m��u�T���Q^�fv}���Bg
[��J��N��t6Pa�C��3H����dh?�O��_�//���
o#A�$@��5���T$au��?]��|��E��Wk1��LTa"Qs�x����^�0��fa�fz.�k:DO�Z����%u��B@�����9>�����������A8�,�0_b���g�&RP���Ne�Chld^����Z�`��ZH�����W���@�:������{e�
�/����^�<F9O��&G�18���w��7	vn0��"�+�U6V��/�1���|{��N"%SGa)�`�`6�����O(���f���b��oj���+q�8f#�-fP�e&�f���8�Z0#(p���L���?���/q��EyE*/�,��O�z|Sq���b���g�B��/�#���#��|B���G7�;���������JJ��&}�u-q����HJ���83��4j%�IH�,`p��5���?�t��E�bI������
��
����2]o+��������_��������� �~�C��q�t��>E���`�@��3�L��c}��Y����L�=	�
L���,��}�M_Nn(b�x
���y��k��1�42j�6���ai�ap��5�VI�|����	�6d�_�#����p��6q��3����.@���#�A[�5��R��d�2����F����j��}��v�y�����h�7�r.���Z�.����%i�&��W�K�m	�~����A_����;P4�$��v��Z9f8������B�������:-JN�|�����C5i�tND�����V9��=��S�=	d�(������+��=��b����3��r���f�Szr`�JbtX�P��g>�����VL'���}�$���=��'/v��E�l��-Q��S;�Ko��J�n��V�f@��0��Z�6�p�=
�_>y���<��	|����@D��%E�T)M�Q1�����=��c�D��T��Y�� 2��� ���z�Z��N�_���F-�+F!w�{\3�v��n*-�`s��rz���9v����C�kn����y��v3
�+�����������Z�U�+.��iIR�%�g���_�T=f��m5��3t�.S[�j���}k��:r��G���lu��o�����E������1���ypy2��eP��e6�!��A$�]�#M?/��6=p�6���vq�4��Th5���	�$�S���9r,��������\��R��z|w��#B���B�&���v�����;�M�n��]!��(��q�rA��)�Zz��k��xZ��u3�2$�}������[������4�0���w$�n9�4��Al�H"���rhjI!��>�q��jys�����u�c���=��`��w�s6D�K�p:l7�GLT�����!��(�b'x��R���p���2B�����`�d,H�^�����O��[���4��@��)��S1��Q���	e�
�Q���I_"�_�������$�6a���������[��<��6X����
���n�OY�K�k-��;������Z�S�������Vhe�V�=}�:��d6d�_�yV�V�'��L����>4��������Df;��@��`lD[G;���c��*/4�_�`�9�fz9�lTUI��:`bXgJ]��;iU�P���&����S/G���8��1����3�+5s4��9��
n:+�ev0F�J���v:;S�~��m}�(?#�;Co���~����#�%��.Edj�_-���K��q��-L}	�]���f�����]��6jw���<������VD���(�\��]9P�u5�O*���{>%D�j(���^u]����6
�e$-��vwTX/�u��D k�D��;n��rr�`�nb��:	*pw�G�_:�8b(nG!�����kVW�yg�v��~
,	"�0c���:�/����������.����h`5�W���vS &�0��w��c��2�cO�
11�[��V��&��9r����1�O��g�A�^HE�aw�|�1lA_�����h��=��-ed�N��8�m�����d�p���3+Qhk��u����.z}uf��W��������t(w�����X����E���w��,�`p��
��5.���\:yF�b P����J���"��S����Ph����0�+%�?���pDE-����n:+��n��Zf��ZJ	��t*�e+�H���!;L�a�Q*��9� �P��8-�$�j������z�d�a�oD��)�j�j���`��M����4��	�0
�3��f����-y�V�Q��+�.e���![L���5Q���)T
�W�n�;���
��Z�}�&[���[�� NN���h���_f&���"
7��OY,��f��t�A��j�X�q1L����Z�hi3tQ�F�w���f�Q���e�z�"�2�Y
D6Ajs��O������;�=�e9oK�{&��E�	�m#	���F*W������|���D�+R�d��X��>1�Yz�������9{]i��������=��r�Z75�9B���vG��gt
+��G�F�t��`|�>�A�+�O�M{����,%XM�"����H���������[�s]7���F=�h��_L��u/)�9�\�D%@���7X8��A
�^V�:�p5�i
p��8k,���L������!������#��p!�
�Sb;���xU$�c �����b;���x1i,��b�/���.����M�d�,[I�/C��.	&����M��>�t�z��W�%�\������$5h��q�h��I@�U�H�?��3�����
@2��!~5���)M�
����4�&\u$3B���-d=����	���&���{�(MX4F7����Af�&�n������_B^O��Y�&\�6g~�����B���-��P�0�������	���M��P�0z������
@2�@�	��`�|�L�y`Q�[]�"`�R@[�}�a}��Y_J@'v����[	A��\R�!�QG����|:�s���Iy�'t���6�@��s�o���mNY�"���D�W��V���r�w
�>���Lz�s�f~���X�P�C���
��!�V����9�3jx�����5<d>��l���"�V������3jx�L��y5<d��,�4`u@
��:�i������T��W�W����k�jxX��V�_�
��[����au�U]����k�jxP��^�_yX��X�W�������jlX��b�_
��[����-Z��X�W�������jpX��b�_
�������a~��}5<��o���i�Xu�Rm_���[����A�}Ku}52��o����V�-����5}�}M�Q��G=�� pfJx��^�xYt�^��W��+�����iO�
�7��X��@��%#tp���4��	���@����aBj��50'��f��7fp�3��H��5~
{��oq��r���}]�A���w����{��()�a��7��-93�V��xS�cw���lb������+����s�l]���K\|1V�;i������I�t�	2+�g�6tv�XM���M}�L0�-�p�0%��]�0'S&4�����l�ui<)����09�,��!���iJ�(kp=�iy�!.n���)�eo�N���
����_���p>&4�f�41@_n���
q�>w�FU�+�����C��Ydj�a�F6	�"A��������|�����y���P2�q��Q�_�"]o�s�����AhMx�>w�f������W?�(�����m���)��)�����8��,Bk�#���6c���W��xt�%����;B��i�sk���-;���}�m���r���`rz�
���� �c�����
�@_�����%�����Nz�9��JoR���y&(�7�O9�e��?
nR�j�atDk�7(D9��Lg�65u�fi\��^�Ca��Z�%Z���t�>w�F�&�7V��un�[h�Do
p�g�6w����~��bqkZ�Ii
b�:R�;���j�N�Ybi���.�/���]4�q:�TW�����@�P:G��\��	S����s}s&o��(�#�p����eSy��i�Ivq^�����W/�jo�N�6����=I]�.�V�|����2�g2u^���Z*�������g�PS�:Akd���,������bx���,���n�|u?��bp~��]�$���.������3yl{�.:��E�6�r4p�&{�0�1;Bl����\�-�3MH:�q���f#�/T*��eb���=��Bjz"ng����T)��b|.���w2jG�M����N*/�b��
��c�
�\�k��Ys���T�a����v��2=�w�����D�G^�`����4���4����b��'���u_��|:��W��$2l'���ID��K������Qo�+7
p>��M'���b����R ��8���V���>
�k��s�\%�n�.��W�FOY�Q�|��N+������]�[�lM5���C���R ��(���U�E��R���iQ;�3��B1����X#������`�����KTbq��Z� �F!w�{�%���*==	������iIC��#w�{���az��r�W���6y�7#�Ixx�5r�.e"T�@�:���s��Y��a^x������o��C������+M=F�w�~�@��jP�gk���9.\��i(Mt��`�����}y����������&o���p{>�mdH���Q/�j�
uf��p���@�ar�|�'^����,7�I�H���\�w!�/$���A-��
8��T�F��V��"$s�����6������n���;���2z}���py�N�U�m8e�����p0����c�;>��t:����yO�Vdw���H��q*8U`��V�h����z���Q���6�;�F-�|�!iK��:'�B&H�H%��`��
�hxq1�?G	��G��?�SX��g��|H��7�+�*�����W/��]����mA��|�q�p\������z*��k�fky��g�L��C�s���go���Z�+��J���V?8�����7~H����N��a���{����(��/��t���,s����� z��e����D/���������)�ko�0+�w��+�����6�j��=��E�n�U�?�y!��?��W|b����qR�E@����K��:�u��������$=��(�� ������QE<��������#�QE�8a��Q�(>���8�����S��,�s���w���~���s���������y����q'Hh|*����,<���@�1���s��B��������� ?�`I�-,�w�"��z������e�$��=�������O����Rm�>������� ������~R�V����4`��~�p�h�_�GW9s�y��1�Y���=����-�N��������g�[�����5��]�)�����x� y-����8����������=���8G�E���!�6��T��4�_�?���bl&3=O=���C	L�%?�O�C\}�k��N���s����(r6�/����"b��o����*�9?���l�)<O�U�+����Hz�N�m�1k�������]���/3����o3�{�i�k%A����-
�����)��y?��@��"�F�����R�����Q�<��.}���U�+��;��Th��%�S�Z�9P���V���7�*�E�Q
)��H��VG@w\E��5�3s�*��G���(
�<��1��AW�����c�G�!Kc�J4?&��F��U
D���!zW��3�f��*�u�aj>JWR}x�N5��D��:k�r�
#���C�z���V�C��aO<���Y���9�>��G�����q�����_,Y�y�1�o��c-�P;����r�}/���>a��V������.���������(��k�f��)�-�`���s=-_�E�h�"����vP�O�~o�MgZ�)�8����4�*��r:Y�\�Q�5����Z�GY�-$q�&��P���(g �;<^i��|#s�^�L��a+��r�q��T�����VC��x�Y�G`t=��5�{��W���p����#������
{��qR�}��]��b D������w�#�l~��U�����/:}cv1��g���iw�V%�k�N5��?H��E������m�vB������p�����Fs��^����6�w������]�}�&���E9��������1�%�B�Z(fuTb������<�tI %h�@�:��`����i@���d�tt�-�p������Lq�@�w,�K�(���@�:�W=k��j@�;$�%w�co��[������t�P�k�4y:�l�"6V��p���1�`��[�smT|f�6}�sG��:���sy��.KO^#E���^�6L��:�Y���S
F��E����sz1����'�)��t���r{]�k2������S���YV��j�����(;1nin��<�B���c�9��oB�����V����s���8��B<��
u��f���t�J�����`
6<�%b\���%~�Q���s�8�b;wCZ#blvg������vl��5�,US��,�����v
�`[��s=�������}�77e�,:�6O1�)<4��7�;�W��foH��EJ��[�����?WV�8����)eRH����_Y�!f!�.Xi&e�d�8|�����������dB�W��
��Jy�����<��C���E.
S]==��F��vx#�o	��c%���!���B�0�>�s	��FL�s�Z����/7X��v�,?�s���cx�}"�{
Y��<Q~R���<L����<b���Rs0��2�3��P�K?U��/\���	�L��h��)���8
��])�~U/
F�S�.*]�������D�D�2����Dj)U%�,�����hW�����rwS�hq�W�t�;\l'�@?��6e�y��]��
�Q�+n!������������Z��>q�;E����76�
������J�������8+����\li�P�Owr����H�{(t=+����Ez:�~���[�������F\x�l�ZyJ��������Y�Z;��R��7���X���eN�9G�lo��]2��dS������P��T�y�j�����8�Dy�H�
�u6?�}y�m�UW
�HYd�6�Z,��Q�8��9��T��������g���,M�_G��:����J>��l��
�`>Z
�J�y����'��,��y.����D����K����"�y"�y�x���}L<!M��E���tI5���5��]@6ix�x�(����)�Y_^;�tN������!w��+cj���V��o�����=���"-���iQ�X����8WD��?�E���Y(��b�8�Y�.Vh��������Z*H�xE\��<��=���������b�>k��6Lr�s�_���H0���;��aV>�K��Z�c�*Nv�'�	���-f���G�m&v�����7R��*W
����P���~���4_�Rm���a�,S�k�F��Q\����>QX�<�����]��f���)�\#����!~/R����]�K��}U\+b�mk8D��>�6�r�kj)��o��kE��m����8M�
�9-jww�nK2��m4��>FD����v|�PZ@�����8M�6��V�JM�{�J���64qR�c^���n��K
���=�rzq4�@G���(#J��Q��"H1�V]�!�_FV�t�Q�o��}T���5�/����E!ZM��6��o��]"��PW�w�T��������:�wW�B��@�Z(�|�O�Y�]��|�>�E7�L;�!�2����z��$-W�wWD�h�`+���m��T���"���^�~|g0s$u����U�Sd��p�O���l�)����+<�� nu�l�<��\&�B�`��]�c"�#� i���m��r���p��/���d��Azr��&��4b)��l����t��4��9���O���vy�/�vkFewq���G�L���5�/����#�������E��Q��E����5�9�T���V�5��x��
<��>.�>jtS-h4o�^:G|T%lkT��@Wt��������e�,J��.zg��qmb�MPj�/=��1N��u#eA��aZY�~��p��(��{	g^o-R/O��=��q�!M�^}���L�=�))��."q�Td�����r��������dp�6�d�E�P�U����w�<�Z���.�/���B�
��k������L	P�J&��\p5@��wa�=l�����{���`�`�����u���c
#���K0�X
3jxH��d��� c5���!!�j�Q�C���������E
.6CK���?��r�YT����77y���dzum�-+�\��,��D�n��U�����!��)t=��S�,4��+�F%
�?�+����j��W;D��x��i�m�H�F��Z2����dJ(h`�Q�bA3��`������G�d������S��9��}�
p�b+�����6s��m�_�IL5���f{~�%�c/xA�3���bP.P�V!��������j�B��"����e�bP�(�b�������un����_ooV����[�J�87�����S��
;j�V������5�&+������4r���VpaB����y�(�����R�FBG�����1K��o�����{�`2Cg^���mX\(:���Us&�N�\������O�z�����?�����o������Qh��)�1�"�e���D�|���LC���Th�j�n�{Y_����l���yS�6�L?��21��/���@idBA)r
i�{�Q��(�w��2m�g5F�g�&�z�c�����������b���MG�"�J��Q�'Y�hae�]���0y����^W��XRs��Q�%g#g�A��h�`��]�?�Ee��c��G�������|�����/f�d�
i~=#a�\���
&
��h`e|����������:{%��}1����r����-���#B��5m�R
��KV��2�,d3�d�K��WC+��6S�gm}�i3!l��B�@�:K'`5o4z�����}~�����a}����]bi�,�b��B�	�2t�FX�
~z#��/����](���Bh_3!����,�Nvy��5�$�.��	kY��W3x�?���dd��*���l������@���C�";'[lU��5��x�	���u��~����)T���*��m�G;�����&�_�v
3��}*��a
$��0������Y��������Bo�io1	��g-'�Jz���u<b��t�	��-"��*�\���`�����f:��A�<����w"�}N4z��F�oF�m��c�]�U�&��oC>��9� ���u����������4@���5�h��Z��vWOL��Kzb�U��,����h� � @$�?�5�<?�������
�*U��������v�.q�c��^-�%����k�k�Ft����N���I��+�n�O�����P~7=�T+s��a��k&��Wc�:�c�V����c�V��8�C��/�W��EW�r����J��A�2��#(/e�o�I��Ys��d^�b� +����9��J���,�I�����t����2��~S�����
 �n����u�E?��32��tx����$�H���8���mv���%�M���~t2g;��&NZ4"��������'e��Cx���
��rmh�qyXVI$W�`��QW��r��B�?�\:]n=�h�+���[!��?��o���/���������f_~�����zcJQ������Ei���(D�y+�P������f����C�y+�P�����	9�"0H��,�����>��wF���}���d���n�)?�P�#����Z���	�B��T��B�Y�@|V��#a��w��v�g?��*M^��t�S^�Oq@��ouj`C��P�C8���_~�~p�C�%�"�l�������������K5Jy������O�c����[�uv��&U�!��)���<�a����z�#������W��v�����G3����������kod�+x~0��K���|<��a]�8'��"��T8^��Vj�X�r��H�X�&R��b��M�R#����(�F�E)71J��8F9�P�|��.~z��(��#��~'�1M��\�F~��L��d��)�?�����ZG1���a^�/o������*4=�����s����7���|��%�����@�#�9�mh��qQ�-��i�~��7��
~������"�r�)�U����EYgq-��p�Z{��U}�m8������,}��[�������X�����	vvgeM���+��uf~u/)e�>�D:��f��8�Q���n���!�LN
�kUS�r�#�r����T�-&D��c�Dyp�����L�	zP�7��0�?������y]���A�ZB�/&�2��e����,N�P��Z������������A�5������X�t1;t#.��bbst6�]\�X����?:��� P
���(xC�eC7R%N?n��x�y|�$!<�K@�kE��������k��}���
�z6�����7��V(����>�I�x aw�|��/=�?����KR/��42�o�^�,����C�(P���%6�U�/���=���Cm:8\'`k��������<�j���8�\Pq1�?���P!��5�K���������[L��_�������oq<� ��4���}��>0�b�c����N���|<Gy`nX�����9�F�������1�$�����/b{xL�B|"�.���G��C��!N��S��x���:+��'�������������N����������C���cl��5�%�S��$l�~^R$>Q�&�~I��U��t������z[u�z����[5F�4�=��8��G��Q���_Z��1����!�8���w[y�7u;�rS1���Q� ���
�<z83��Szw;>�{H���??���ys���t� ��\c�K!�"����:N'���=O<���g��yH�?oX�,���tF R-���������/��hM:ia�k�,:�"	=����8�X�~���>��QgE�f�B��2C?�E����������>����t=����Oa^���YEK�\���-�w{_�w�^������Et���l/��
$��E��#�'�"�\(_����%�����^��DZX+�Q��Y�������7�xUY���e��&��,��������s,�0���/��x�\>�	�,(��#|b=u�X8,�1:$���k�\;�s(^EO��4W��2Xm
6��b��R������X����������F��O�z�X�
��(���\U�ya+q��S�E��'>ANQV�G�W3�Qv�E��v/�g�T%�!��D�`��4U/�E�T5��������~8=�d�k����V~5rd�ds<���b93�����_[%XUvYU}\�B�`:*s��j,Ps��P�Ck�m�C�~I��_w?_�����������H�,�9�LC�Z D����;b�Tk��������JmYF_�/�aIM�ET��W�H�)F:e����F��^Z�	lQ�}�k"U���5����_�W��e�x]6��
�E�k/���zr���mz���<�N"lg�����()X��
��<~���=�=3�i�M������D�&���T����.����h���b�}�����J�����{����h��M�}��U5��d��B%z�t�-�	�MY�4��f����}s^5f	�3��f����mM�q�h��W��i��e�X�^����z�[�g�d���gvGQ��s����������WYXS����45J]r�C��(��Y��O�x�xW�P����p$_q$�R4��cY���������j7�D�B����l��B���.7O<�UO���:�l��o�}�?��T�1�rEHW���m6�38�D@UGk�g��4�:pG�[�T���TyFz���H37�(�����;����z�gZk)�DG��R���r�c 7,C_�:>�������\HG�#�;��aG>7�u��r�|���Q���(����hj�D��lR���O�yos���!K+kS��p�B����j ���WK�|#jB�r�8��Q�]o'�|W[�	N���x�A�����4�):n7�54���>�������Q���'>���b�-�@��q����W�oa-��M2����8�����
��.��sF���?���C���Q�x)L��@w��'�q�����Z��U4����2=a4�>Kn�eW��S���������r�g���\?��D��'/L^u����r@�	i������������������N��W�D<�&k����rm�6L��aS.j5�����xABW�

�}��V|�}��7����;e���E��~9G���L��]e�8���}:.da��1;A\�R?rA��_$��Y!$���xaQ�O}N~���=m���'m8bWx+�5�bb��z�0���oG��K��L+��S��)!�ZAs�j1(8P�0�G������bI�u������8�A�(����������L���i{8��K��1��
��
���<y8���An�=m�����Z�B�y	0)j�M)=�+��ts���	 ��THc�B����.��S�������r���gR��e��-(���PM��6�J����@��q�
�W�,�W�F�����7��q�}�|��b~-��0����.�[�����M���u��V\$%�H�hs��e!�r�����F0�;��`F<�2f��m��O{�i}K(;���(F��u^�������A��l�A9n�m�c���9���
��yp����;"Z�0>��w]�N��z����R�'��=�c��1��WQ#3h8�z�qr�8q.}z�#��A��s%yN_'������
���b�oR�*�WAU�7�Z�n�"�3�������gl��o�FL	�������_<st^������P	`�CU��gA������H��7������d"��@kyX<�@v��������.y3�+j��i��n��jR�I�'~�A}��� �,>��g�dOF�u��i����s>���~�@��h��-~�\h���v���
u��R=>@A0i�����=��[��A�JH�+��l�)�_(w:��v�h�0�*lG�����C���u>�mg'?�����P��
}z� ;c�p�B��'*0�J�����m�l��J�)<�fE���	~]9���Ea�P��[Oq��5-G[���k["�(?��G�#e��'bv��b��^C�X��p�{�����f��O��b�u!���
�~
��X�6'�5J�����C���Pl~��%b��*��A�/������v��"(z�|���������#5;�m�����Y����ZMz��Z��%rx���fJc�ZE�R��|]_am�f[���|v��%�z�R���=���.@��u
�2���@��u��:�z"O���X4���)Ku�3���-_J��*�O�y�>��>z����q���'���q�q#�m��s�Os6�>�k}��+K����LU��*�o�b��5���zX��M�_� dObMnVey,�<�}���<�+%���
�$@��6)*KP��-y��`c�S*��J�J�Z��`�n���
T��yE�E�[������h�eK���o���_pK)�P���!�O��fQh>��YOA���D�!���)n'�`5�����n�hE�
2�	�t�>v��%d�����CQX
!
�P��A��oq^x�Q��3����Cd��B�^���J��
`R�P��Q�1;s�2��Qd�)�������7^W�e�N�����^�/�&v>G�+u�����&;�E�����(u��z�Zlpj�?�;�+������G�t�,c�k[q�W=?��������o��K�s_�O��@	_~��I{�/l���������������v����"���c��3���G���/g��	(���>��6���A���H��g}L���B���z"B�PX�'�0��������W������h��)���f���oa/�
�1`�����o���!f���e?�w�������/6��6������4�:X/`~2��}����x>FE�����/�%���e��g�%o����pc�������e�d2���I�D-7H%�w�{~$�6�Ql�e�#�0��e���l�G���qL���� �����C�RR��#3#l���pN�0�x����l<��XUd����r1���`eQ��������U.�H�����u$ ��Dc>�Al�/o8�e��`��||���D��.C��!�m��y;�����z���?p���4�
x=��Vn��J_��ax'��@��-�	Z��������C���5���L �zd{�/�����O�l�Sm�,2��:j�~ua����������N�����V�lL���mz�������e�i�}�YZ��&{��������,�Q����*�>��>�3����I���%�qb����������/l�+Z����h��������
�J��%����������
L�WD���?��V�o��i�34��fp�#)��GRJ�#�(qZ���FQ&]���v7A�]�$�\����
 ��n������\�gb��fx�_������ ��%�����E(f���-?j�H!M@*-o�����-,����tay�
�[�x����Y��k��A�:���@t]7S������U���O�'
^mA�����/3����,3��#�O���hh~R"m�V�M@�� k�9����8�6}��<i�k�p����,>%�p�V�M@���+-�+�+���K�����o|#�
��qx����Y��=����Ofb 9s0�5�q��8<Yr�>�fu���Z��N����*���+��Qy:_�}�X{���Mn���v^�i���U3;�e�8y�3�&�������i@����X��gbU6�fb�S��]���t<�2:6;���j&����X���gb�s�����@t>�2:2;]P��l&����X��
gb�K������@t>��;4;]��t&��fbU�7������w7k�3�*����lB����X�w���Z�22#�Z���@t>�"����NgbM@�a&V���L��8�u:k�3���'� ����L�	�;����9�3�3�8��L�	@�3��6�gb���8�u:k�s�*�cs����9XZ����r�������9XR��>��&A�`�ws�����9Xu�c:Ktv�s�$W�<��f�s��p�mzN
�9��m��������,8}�"����o����>Y��(3���I�iR3{{s�_��}�2N��'K?�� �����Y�*�!�t��<�w�������_��W+y�J����>�C����/VX�m�/a�9�g����(S����b`�yc���V+���}��9��?���z�
��]���]�������r����MQ�>1U��j�����5���`���#U5t)K��/�����=MQ�>�S��b��Vlc�r���P�^%0���
[����M�������1U��Ls���5yx|���1L���24��
�������yr��t�*x2�
)DMG���2��:'Te�R����r��t�
*�2��y�uR��L9iS�0���aj8�������&4z��"/�2��(���U57!���~�0]�~�q^��~��!e9��)����h��X�j
�u"G3�|2��.6��t��������.S���+����Fw���0]�JD�O��G��L���{�^3�)�P���xd=�M&1�8�8�y�=j�Y��|<.�/�q�qS��}{)�WG��S�w��T�m�^���|��4�y�H5����<pmNZ�)�;�0�a�'y)a�\��"���������T�����^��������ahN���,�
2+v2\t�@�4�w�����a�abe�N�y��*q.��c�w���S���Q�QRe�nsr����y+��73�ob���1���|��]L���|�mF6_����Y#�ob����K��]N��|�mF6_���?7�&���l�[\M��|#Sm=���������e�rG���O���u��nb~s��C���i���M��������Wz]�J+7�	��O��F�j��U�
7�P/���}_3�@��uy��.Nv�)b?�B��?��{��������O���_�Q�>r%��I�__�4C�P�C:~�������������c�����tw���r
=�1-]���o������A������m>c���-|��p��a>�m�x32�����a6f��Y���0Cz���Yl�M�,*�Y�i��ocF���y��)z�$�b�
Q��!
��C�|x��'Q�y�Hp���(;�w���x���=x�{9�q�����j$aq��6:o�1�=H{�P��%u�jP��oC�s�X�A[k)�@(V����Q�h���M��%���&���Y�us������q���������SX��l�;O�R�P�R��W��=��j{�����`#g`#��v��;���
��������d{<y^qI�1^�0(bP���F���72ML
ob�*��4/5TR�r��������Z>�.�����4�����'#O�"?�N����?����H�2���"��T��u���U~{ �X�v!O���F���Xb}!���s��5j��_./Q�]��S��J�����9�j"�l�5<��I
[%����=�1�������Lj����3�-:k�J�����R���J+Wy��X0���s%��f%�K'�l�:�I;D�z�k�)h��0�E��]�_�h6��k ��6�0�EH��zR��6�!
��A�vRl���0 �
��WIAm�a���b���b<��h�1��.B���u�#������1�vRl,�h���(�42_�m:
��j&���F������'�L�e#�6�>�ak�����V��t��Y'&����$G�����0��:@ZY���~��h�O����v����K~I)���$�f����jd�j
`z�2qa��S�A���F���:o��@�\�|���Zi�u�Ng"���tS�����S����0D��e7����i�%�1bo�x��{��*�M>#h$0bf��Z�ij}��>�,Zm��G?�0�IAh8���j��=��Dc�"'�l��FF���na��N<�|���Zi��Y�m��DMp[�m�F����Q�).��(B�m� �B��x����N�]�������"��I)�#��"4	��U�2r|0�����Zi�Qp�6������H��2���A�
���[���]ck��tpXb_LH
�(H�/����`eqs6Y�gx�8
���'�@��1�Ex=\0�A0-
s�d4
� -C42k+��R|	r\jT�U��� �������aUa�#�)����
k����f��:@j���y8��5���!���V#C�
��[	��4�Zjf�}��h�<�
)���������,�61?l���'���_�=��g^d�����k�[��T��m��������S�<��5���Y�����v�����cu�A6/��;Q���_�j� ��!D���x����!��2�lY��m�v
�&[��q��"�^U
��8s��(���N�����7�B��0�����!{�,~���%����*���+7s�=�����b7�?@J��r'M*��L�~��'���������yh>��6'y��mi]���%u�&w��Li����eT�a�%������.z�3�94n���S:���)�&:J�;hA�s���n�&~��@�B���c
��9��@m�M�&9�1����3�Gi�?�����E���9����'��b�	�(]���>�V��f?���
sy{Z�^Fg�����.z�)�/W�eN� &��4���������oO�X�&%�����d:��br6i��&N�L�������6�fMr���D(!Tz2J�(
��'8��.�r�!L��p��B{)������N���k��n�S:�0����et���(M������rv[F�DPa	ZB;N���a���������unJB;N�;hA�s��.n�&-��)��Q:8��z>�f��I.oL"�*=�p�N�	������|�C��l_(=���]��U��n�S~6�����W�F��-�����~q9�%�p�waZ�:F�;�`���oL���5���������$
bpi��2���������a���MoJ��)��mz��	�
&��u�c��Q�q�q2�LMHG�s7m���yc7�i}>�0�k��P��7
�PGks']F�;�_N�����?�EH	�hm�����������������.���v���n�,-]��1���z8��z�e����oN(�Hj��s��uA�Pb<�L�U3i��D���|���)�\���@L�o���p`��|�X=����-�T}c��3��v�[,g`<0�R���ws�����n�=����[����R���G��r)4�v]���
O�o������[������Z�z�E=�f��Q3�%�� ���Eg���^kA�+�L���ucG�2�W9��_�9�B���z�E��)M������p�^��JlNWi����:�h��W�d_^��1y��t0e�\_`����q�#��:8��D�PC�'�`���B�@���w��`�A��I�����-���Z���b'�6�������g��^�IMBG�p
��!��U����F�O����qU(�6�8�S7��������Bt5'%�#4p����r��j�����n������ilp�x���.���C���%kMlzC��E
}E�"#w�$jp��RV~O�
]����E�e�jKu1�%`}��ktr���
����,��$��/tE(������X����1�0-i>HZ�v���#4��
��y a�-�X������r��-$�|�P]���W�\o��9�
���D�@��iJ��rC7�M�c���_�+#��>*���r &HP���x|-����_�q��e6y�f�W<G�Kx8���e��)�>����M��,��8M��>Tc@Z����y�����q)i����i�!��*�"<x�8���K��9?��)�_�)�������I���$���1����4�� [�i&5������:r(����	Z����:j|q6�hj����,�zH����99�`.u��C(N�T�:��n��Y
�Y�m!�_������s�����c'_|�����vE��MhSQ�%�����g~��">z�{q��>i���m�o�A�:@���I��U�r*�BC�q�&�Ty#�B�3SO�����w��yxa���<O�qX�K-9}Q�'��?D@g���x5��<�l�oe��Z�%%jf�f��;���Y\�!z��c���Q������y��"�?'q��e�/7��_N(*��Qo!�H�����zd4U�����{�LM�o�H��!����ak�86>�l�HwY�h���,��]p[����e�!��,$��r��Fj&�4��=0>�������~������,��Gy����c|�YH=�'=ge�/;.���F[J��>�A����J	��(���,*�3��e�O�!J�t-|��t��RE�r�Y���]�,�����f�2�W����E%b�!��LU���IT��PW�����yC�����j�o:�z��S���j4���a�PA;��n4��j��h��E����>����O�����q{j��k1u=�L����������3�G f�6�v��3K��)� ���t�\L�O���ea	������}��v�/� ,_��lZ�C���G 3)�m1��!�U�`��E�_������4�����H���:m#6u���	�
��
����t�0y5�-���Z@g��!�TA<�A���RjJ�MtF�\�`�,_	�t��_�Am
��
����fj��\b�	�����b��K���-�XQ�@���������,=�����Bh��A���L�:��QfV�����4R0�R@Z�hdV�������UZbP��0m��n��\f��K���Kn;]M�������:Z#c�/�sag�Z��.MD��Q�C��6��0�`��p�44��Q��ld��R	�{e�JA%5=�3�F�1���i��\�%�5�M�F��w0SS����&82
�:L#��z
9GF���O�O�RP���!42��������H�M��e�$:DG���U��+P5�@��P�52�����?���u�6��:@jbdY������v������Z�gd�u�\V3f���F�
�:���]����i����V��0�R���id�Uvm_HfY9��CmLk�����j�%I�zV�Kt��%�2��FF^�������l[jf��M_,���5Zi��Y������2R##/��I��5���c
ZX
�zY3�E�6�����'
a����v���K�4'7�4���
�h�O�y�ENv���L3ta�&b�����A8���0v��)h]`������~Sn	��C���9�y��r�7_i�u��(KR����Xy�aa�kWK��D����C[1�?�������7@��m�9��5/��u*�*RG8+~�������_��m�z*�}�������b�������������^�� h��Uv��o����g�w�#gF��7[Z���m�!jHsl���,Y9j�����S��=f��	���{���1���rO`���W�[Sy���Pl�'LO�n�Z�/����z������������[�_�o�D��5��{W��]E��/���U�#\eZO�OF�����S9�QJ�	u#���h��M���b�69:��j��H�,�aS���(��M���b�65���U���H����sx���"��l����������dJ�V� ��Z�_��0�{4�T+]a��D������(��+��3��|f����'��/�������P0������FN��#�99B:��j����0:��������FN��#�9=2�����r!ZB�~��TR���G�O�d^�����<r<"_t��@�x)����w�=�UV=��]eOs�ux�xB��t��&���D��H0�����h��M���b�65:��j��H�,�aS���(��M���b�69�����q�u���:Fj,&���D������i}������.��R����}��e�+��|��@��>���i9�j��f��q7<1��GI�1R��#FH5rr�t�����atT#'GG��Q��FF5rzdt�e��K,����:K���y0���+�po��n�B�N*����bF�"���bD�L�������*��C�����=T�H��Q�T3�>�����������Qa�uPU+0"���jF�T�U�����:��N]S�#���P�V`L(uH)u�y����;<���.b�IE;<U��*�wR�U
�*��
���b�T�� �OUU�{�@w����p����Se��y�K�a��Qa�uPU+0"���jF�T�U�����:��N]S�#���P�V`L(uH�VnE�6���WY�����{:'[Y���gg����o��D�i�����������<�1\7`
���K`��8K�������^�w���0J��+�Py�(���}��_��6�;�kd�i=���]��Mqe�e�F��.��g����g
�����E!Zhd����@zhR��^OEZ�gd�E�vF��)�3���-42���K�P���+�AQ����sU�]�<8��]%5(
�"@#��+�t5;����h
��9�I�_�m�f�	@/
�������:T#3�]��5�k��+52�e�F&�q�)��K�7j1�qa�6!�uZ#&\��k!�Qa���T.�>���7�7�����&E!Zhd�y�r������RjP�5xF���s�s�~L�JV����z�� ��8P�0���l�F*W����#�)>���
���b8��;��kD�RjT�=xF�\�h��-�KJ�1Ax����R.�e�j�]@{N��vGhWV�H���w�Sc��l�������.N#�L#R��������E!Zhd�i�����W&��?*�AQ����S]�iX����
����i��i�
fz�j�)�aa�6!�uQ#^�<��l�A
������3�c���A��hdV1�9�qR@]*�(|5rP��0��42���L���ak����A��hbV�J���}��W�����|�S#40�
X#S�
v��,���,G-�0��*F#����K��p�)�-���q����{�B?�����k�A��������b�������a:�{Q��t^��O���:e�����32��<�����H�����h��I�^���U�+�AQ�������G��B���.�=xF�,w"���2[FR���Px����R�$����H�����(@{�����h��i�s~"y&
�8#S�!���������� @{���9-��=�lnhK�L�-pF���]b�S�n)E�`������i��q�-���j`���%5-��&F#�.Z���9�H;&���i��i��62������`bV���B
N����W%z~-).`#_[jg�m�F^�`�c3o����8N�(M�;��@��
^����L�����zzh6��������u$����_A<���|]����m���=�k{`S
)��"U$�K��;.A2(��C��AW���%��s��#n|i�B�M�;>X+W�+��(�2bbF����P?������s���lu��nu1��8�\��D�{b��%��8��qZ9x���V�_4;�{q���l9�X�v1��Q�&)~�W /�4K��IH��y;~�����<�_�qQ�8wS�L���s���^���];�@?!;l��M?q�,X�FK��p`p��?�E ,7���[���	a'aH��cu�����8}_�o�#$���!�����bS[p���jQ����r�VCOe���a����sWrI+��X�h�\��B�C����`+�Ou����<	�E7�z��td�V�+Z�s&
F��!o�4C�q�#��r�\CM?�fW�K3�:�9*H+��=r��d5rq�#��r�RCM�\�*����8�QAZ����*�Y�D��E^�5L�������
�����A3�:�ud�V.��%qA0�,!aO6H�\B����k�f�b����_*3�ca�cB�q�������6��2V�@��h�`�rtu�����fS�A����[�;���iK����A��8`Gp��=����8�)��^�q������w�wv2<�%����M����v��������o�l/'������vt�Vnn^g\�?4�7C����i�]���s+(9�����������\+w�W���%mt2����I���� [�}�`@�pVA���z��ul�6N���X��#m�c�������Q������
2y���,�WYA�
c��S����/�hEV�*;�[a��b�r���< Z���UVP��GDh������N
V�Yiu*�qL�VN]��������R�x���R�2�j�"��*;�Sa��b�r���< T��}UVP��GDh�T9�����fn�T����n�Q������
2=Z-+�TVP��GDh���D�wIw��
�&�N�n�,��0��;�P��A�Vm�����a�
�����S�gZ�-R����zF9*F+��+��<���C�,�qD�VN���S�����h7Hm��%���c�r�R�MM���e�������r�J- 3�A8+$,*;�oa��b�r���L���=dJ��GDh��MY�8[B=oHL�0�Q1Z9v[A�����}oHL�0�Z8u9Q��,O�����gk���n
s.	��H��<��#���-��]uX��b��qZ9X�?�4
>�aT-o�B�LA;>V+G����UE�/
K��)X�Fz�������5	����,9U� �R`�����
3y!��B/����A�	���EY�b��������P
S�{i`G�j��u9yUwm�Z�au2��@;U������#����k����r>'�����0�3�-�_�s������Vr/9�����=�\�:����_"�g�����(�+��:��)���h7IA�o)X�\��r�����x�W����Rc*G�Ik�K�ka���(Q`;]��"���,x�bq!�lX�T��H��e������h>U�|�!����f�oip�U$��S$�Sg>��{����C�����'���o[k�a�g�X5�]a�_Z�(
���RB�io��]���o�C�.e\T]����N�'c�\�,���!Un4����%O��;���a�")�K����#
mq��,$�G�O�(�nLZ���v��f����P�=��=��u���N
��!���O��6��=�L�R�����5���������l���g��f���i����u�T�O�M��*[O�y_�z�?���������ir������]j��I>�e�����/�+���t�x��_B�*��#gZ�mwI3���hY��p�5�evX��/I��#&~~� �?�t�D�����8�U��
�`�����h��$9��S�-s���s3?x~N�k��6T������F��?�/~��;?92&���r�6P}:�O��-m��2j��\O|���$�1����Y�������Q����|"aa*��}*j6b���y=�h��<
�G���H�wm���Q�3��,�����j
7�>��K���%��$����j�=�����\T��,g�x
�{	��E�������"��b0�`�r�iUO�/A��K����s��.V����$}�L�y�u��_��lg�����z����m+;�0�#b�W����F���+W����m�H�����]���#��r�E�_���'�AL���o7�C��Nb�O�L}I��__6���c���:�I�M\@([o�,
y�xd����''N'��g�9qPB>g7v)+c�����'���]���������
�5�R���O�r=L����D�"
	�+��%��������_3���}��A�!�w
}�D�;���'5aiH�]A&�|�(I}Q��������
�x����,�
�n��Z�Ilir��;�>L��|���u�������
\�U�9H+*�GQ*�v�d���_������F��8�ljr��$b/8Sv�����f��X����]��X�9�g��a����	8���\���<
�,���r�f���U�-I����C� Bvx�su^V����rkfB4^�����	pS��q��P
�np��W���t��#(���Eyzav�{��[�uS�
�#�:��Ad��mE&�p�we
�;��@�~�=[����kL�V;.�X6�
����N��h�f��b,�M���CU>P����'���o����yaY�Z��%�!
w�z��]����C�g�������y�d���qm����Sg��1�%iS6��E�g�\�/����0?KQ��0y�e��������|�+n����}X����+�(+I���$����bO���0��>Ek��~��������7��]��s�v{�������4�����hw�v{��q;�v{�/&��]�qs�u���#�4�sx�K1���Xw�u{���:�u{����t�����.o��0�	�9��\?�n]o��q��q�����$=�4��v
3q������w�[������Nye���j�s��g��<�_/#7+��\�<��3�o������:�":ju�#�[���p[^9���[��#{��.����}���-�g�M
��b�����:)#\]�=#��k�V��M�1����iFMm����7�=�k�F�����-z=	D�FqK�����	��s�1�|�+����LQw��
�~b���I���0����^�|�A|�e7a��}a�A76�����'7��I���w�~Yd��\������������vn����W<��\��� �y6�v��w��p��/����q�U�-N��_����$��sB{/c?]X��fI�G�L�\�|o�;ZU�!�,?�/er�P��������Vms����������i��ow�k����,J���r�yY�O<��m���)�z����h���E���X^��v��i�$E����S�tJ�����o���}�+Y/Z�h���~��W�Q"W'JC�0���0���7�����]�h�g��#�����7S>�"H0��(U�R`.<��D�����z��
����2X]�%E������V
Nu�gq-\��Uo�c������w�
y��\�G�ox�<�B{���.3HB����2c%�\�-3V����2�D{���,3JB����2�%�X.�+3X����2�$�V��*3`J_���*�=:��������������.Qpu9d�*(�Y���#���3��}��\�'RD��M�*��6�/��PD
!PN��H�~����i����A�X��dy���P���i��U��P�;�i1k
w��sZ7�Y�]�ET	p��1�
:��'7h� Z8F�e
�?�\����,�]xm6�b<L #,���1�C�yY1���������l�rI<����M�$���3m�Y�0kC� av����lChM`J��v,���c���<����K�M;i�Ce!�v�Pa:Q�������d�[�4 �v�P`>�����,���q�4����	Z$�L��N
�$V�	q�2%#:��@Z�d�h����xg�/�$'��lc����1��5i����8>��Cf� �B���~h�A�X��:$����Z��������e��%���X!����{.*��������^,m��"p��+��b�^��.�w,��4�Y
�61�V�q�(�NTN��.�b��%�T��H�� =���mg$B��*�|;��\�4�G�"\��[�!�MP�Fg�x>�|��jH_6�&J�����^NDA�w��o��,�X�~W@�6�(8����N�����G"r�����i�N������y�Z?u3�M�$�oi�mf{����`�C��`K�'�`�K�7��2���<������]D7�����0������C2�j������]A.������\������/O��
�h����8����8��Z�(�����%L�:��Sx�D�?l�����{������!�����.�d�{W�I��g��:r��5�(5�C�
�j5�7Nm��V'S����>(����Pu[w������A�y
�b�.Q������Vd����VG�Z�.Jv�����`�N\��S��MY�!���Y�~��Y��������/��'l��ou$�v���*y3\��K�^�W~�'��w�r�m�{�r.������� �xNMQ�g/A���2HjG8���J=c����tQG�K��V)�k0�G0���ctV���v�T���5��1R�"Cw�.z��^�0�������WO�n��{
$�������:��2�8ln:�a4���H�����n:���68���������<������K[�Hm���R��E,�>�_��Q���x���88f�B�.r��������kZ3�R�;�=p�c;��R�P�����0
�	]�X�c��.QY'
�M
 ����e�N'L^��_�R���������Q��B����FF��]�@�wZ�|X�6��{w]��i!�N�;����<�����r�=#y@xNct�E��;tQ�������G*r���.�l;�iK#w�J���h�'1h��Zhm���Z�I5����P����n�">�%�������O��io�r��I\d{���Vk����K+��*o<p�����?�G����\r��)����A[��FPgbG�g��e�v����3�R4�������(�1�7����\&��T��W���2����y��������������b/��$��D�>����� ��8��Mo�;J>���C�����wN��e��u,5�;��Y������!��GVi:6�R���v��i1e�Wb'h�G�����8&@���-�fL\�t�u5��P�x�,���L�n&���������K�[i@G�Y������� �By}@y�@������ �[t��#�%<9�����0G ����:,.#2g���������[Z��i�+��!x#�+������������Fw���v&�X��b?O|5����7�Sv����Nf��:�����%������4�GR,������q���:"T)�OA�T����V�\���r�������"T!/�k������F�S���Ox�[/Q~SF9��]�,Ii
rY%��V^��]�y�%�8G\����J$������[��tV��y��Q�������z*Tb:�OD���ygz�����z?���y��_��N�Md:�OD�7M�Zo:imH����ff��4��~2~��E�����s��{���J�-l��C�~B�W����� ~i�F=��Ir��~B����eL����rB���dO�W|�, Nj`B����p��.��v�=_t���G�Ok��}*v��~~�����M>���r�P������>0����>���L[l����T�������S�<��.p��}2_H����i�y�O���,��7?P���������T$�����[�2=���V��b�&*����[=aU����J!�x�(_��w�y�.����{�/����s�c���L��Y/����M5?+j_3�].6��xE�����o�v��������o~��o~���}@��?��w=����=��������i5-7���0�M�����@���X�:�IS{qE�������*�/���U,�����'h@@�+(G;���=f9�A�F����/;�����>+;B�4^n����c����aq���uV�W����-TC*d�KY�,g�)��<�Wn�3O|�W|K�D����I��dwQ���C�V&Q�RpI��+��_�y�bR�P��"*�0����X�>�s�{��|&���p��b�;��]�L(���no��}������{���3���l:��"'BZ`y��2��8*B�.*����
��P�pD|���D^~&�d���
�f�����ut�����y2O�8�._q�L'�;p����y���v��W5�N���Ny����$/�4�E��0���D�?Stvk�T��([��o����3��m\i��-F�o���m`]��(���!�9?:hl�{���v�F�Ay���`.%��Pr���C�E��e��M3�*B�;6�-�s2,uZ�';[��2���D��pv�����O�xJ��a�{�����l��������+�0A�UJ�=�F{QS�����(�u-��
E��u�,��*����$;}������b���O�_�M���F{_�zzJ�$�O�9�����E����~��f�e��1����B�g��� �PJ����CS�4)Z�~k[��$H��[�!�]`n�{�t�3�Z�����E����Iad(��Q�J��l+� L���y����%���3�L��m;E/Ai<�p;A}��\�ff�Y�#�^�����i�YAQ���q,P�aQ]Hrm��"�GV-��RaNUa�3�����������#*	�%`�>Y!����Y
4]�ZC��-g��eb���t��1����lUgW�q�,�,��N�^�����'v���{J^Y�[�,�g��D��2�e/^Jc�;�]t�Y�����7�;��tB@��1����b�dg��A��3����/uTfK��T�����|���a�H^�N����d����x���| @.����q���"��N�aQf�W[�M�f�[
�~��/��'����O���55�������@��5<.�Y�������
-<6��� �)3���]#</&3�PA�"���kz�������^I{��l9�H��y}V��
25cBt^��nf���(i-sn��.
_$-_H���`�0����Yf�bf�]��!YO<_Sp�l�-���4�a�I�:]Y��E���]�Z�#�:���spB��B�
�����,���E&��c�H����x|�z���pO�����`������U���z�����N��x��z��B�a�:�m��J�w������� ��C�7�3�sr�DbWD�vI������jZ��,w����a\���b��bUWm[�Sl��7�_{�����?ia�m �,t��y+�R�,���E���w����<)�|A�%[��-L;"�������j�r\o�~������4��#6���v�g�h��@��4�p��R%a���6x�.SS��/�p�w��V�+!�����c������0�1����(�W��z����kFa��"�r��Y�����������bS=�\��r�����~r����\W ��<y�=��A���#��
d���1�I5���Ng$e���HJn	��H=�!")h�d��$����"�~FO>f�H�=��!"�g��c��&���N�$����.A���� gi�j>�)dI���!�8(�1�����	���N��[�q�*��rJ��r^������Q���f���'{�DX{�o��1������t�%�^�h������(�1�N��������X��J�=4v�������_"hW��~W�W��e��
��g�Cr�m�4�@T��B�Uu���W��KX8�X���[��<.�R�=5���Q�t��Jl����k(�D�Jp!��\h(��|	�dq�K�����8���2v�Kp-��\k(O!�r�gG4�����p������hV�e����#�/G��f�P�=W�1B�r�Li�e����#�'G��f�P�t�#�(�9v��b�3
�:���N%q1�����K����)R����P����W�gK�/��@�:J^��1���n�j�BL^��A��'�V���m^�b����LlQ�{�4�e}�WK�'3�?�3bm�/'��Pm��82B���T�*�okC��\�8��C��,���+��e�zu��of�������Y9t]��]g%�5H����r��BKY1�!FW��m�����#DW���I���F�#DW���i���Z�#D�*��5�b�Det�|��V������R�+���s�I�rA�f�lc�d��s��kF�4f|@�9���yf�,c�d��s��c��0�z�r*�b�)��9a
�����7"8�U����B"��r��c�.	��6�^������(b)�����	��HF���Z�Y->�p�oI���uo�GGW�PcP������-g���S��_YG����bf��3�"��3�u�������*�!N�F�ZQU%�U�~����X���~���~�A~��B�0�n.���"�Zm�;�qD�����*���������hw���Z��n/�e$��.�!��N�T����j�|Jc�pu��zM���%�9��{���#�U�t�i���o�����[��W�7)��:O�4f{I�g)y^�	��Oi*M�Q�"]���8GF�T�h��m�R������zx%�o3Q����M��^YQ���7GS8v�]> �1����^��m���������
�7_�E�<{e�<I�
�L�,���
�6��sd����[U[���Sm��\�mv�!��8���Z�X-a�_(��N��,�iv��E��
�.�7:j���=�(l8����.zg
�O�~R��j�8r1��B�r�b�!&��%��*;�ka�c��������r���d\�� �"w�a&�[4%��0�i�Z�|�t3 ��#]��0���n�&���NH8���v&q�Y�j��9rO	��I������m5]7�2wP��X�ic�(w:9��n�\M�4���Tt��j�����1���[�V�i��f���\�f
�rW�u���>��+��MVR�(��$*3P4Q9HSF�p�r���@�$� E���)�E�����<�Z�5��)x�!!!�W^a(��Xz:����>����S����LN��-�_@7��g��4�6��j{���9��<Lb/�Iz
������2�^:<����g
>�tR*M����[����]r�W�{{Ix_~]o�S�	�`��$�������D���a���a�CU,����j����x���j�#��������;�{���������:ZeO$2�������6������W�-"�?��,x[.�)��@�N��o�f�j}�c������?�/�Sq���oya�d��/�N���J6O
�S9���Tv�B�9@!D���}Vj����� �W�yyR]%��]Q��:�'�Y��&_�p���ei���
�R
��_f]������N.��|���; MT>�N%c�&$�n��<_�Hk �65[M���bp7�?�{eSEi0��u��8�����u��'���2/�[�c�Ty���*n�9�y����
�v�S��������'`o�u�++S��j������&�L�G��c��
t�j��(.�.L��� N�@�8u���GB���f�x���=�t�i�O����9eY�m��L��a]c`��0;B\�|��zr�Ni���gN����Rq;CM�c9i�:����9
���U��*^�\eT��)p�(�&�K�'����a�FL�C� `v��(��I)N�U��UxIf�1aU��4Q�y���8�r�*�
=�#�ye���5t�0��%��,g�u�,G���Z�'#)k��BD�@�j���Y*���i�I���#8���y,���y�M��D�#����������t��zy��7�H���0��������R���O��hm"�e�A�%T1��������[|���-E��	fV�
�A�
h@!��>-��[��@7��!.�����w�v$�0f��E����&��NgnQ�x����/��M��^h	�����46���wk�/t6e?�R!H��!������+c#�z���mh}1��gAz�U�A��F���Q���a�R'�����'IP����\�x����;�Z6��'E�r�����J��N�9]3�����DB#��h
�v6�<ny�	Qa2�O���X�f[
�"�b�{�G?�l�~-_���|`��5��HG�w�T<�]����x��^Y�\������M"��~>f��P��z
P�h�j�%�V �++��:PbaP���j�%�U ��*��
��\Sa���RK*���5~z2������LN��&3L05����0��4~Z2����I�MJR���j�������K*��Z�8#��^��7����������a�/i����~/��
xA<��!N�����?�wp�c�I���"~
8)��*z�p��}D���Y=�Mcg1����/^�
�{	���H#��}v���
4�oDa�
�����0��"�
T����������Eo$8��k�Xa��qlO��%��r��=���U�����&���U�������e(�<O�fB
VPc�EX��yI�<>���K��H��|�N�0���+��My4eoi�c�$�=9G�QP�[���J��{#��E�xT��;��>3�`�Sv����B�"A��dYlI/_����)��9�%�v��_^Ivk��v��T���i�[6Q5��]�i�i+�b�L��('I����)�m�Q���N�Ij8)�YFfW�)*��P��V�tv��a��
�1t�.�V7X�&�V�}pI88�x��c8��������>�w�
\T2�rM7�C�����,�����.R�_�yyj��rP�Cg*\g`�9��W����������\���?������F�������0K3W �7��
���/�$S���?��?~�[p�.�]A7�ifPQ`�c�,%�YV]�Ve��,7L�8'm���LY_U�R|�e��Ugs�O���T��-I�O�>�=�<�i�/pLQ1P�P��r�+����JvIt9����N���4�^�p������h�~�N�PR�����=������N���YC1m�:���Z��������t(�u��3D������dAq��Le����)��� W�C��RG87�(n�u���Q(`�����;����{�=)OC�#\����9P�@��u�p�v���
!�!�����{:��{Xf��AWcH��4�����r��'�Y,���{q��������Vr���31����A���I�����A���e�4<��,�4��?��7�O����rO��M����rF����tAS6�Y��?N���~[H��9���-���)�@�U���D�*�V���R_��{^�A"4Q��&�V���aSm��r���s�W^QoEQ/���s���`{��	R�L�ni��{���9��bE,������c7��
��`8�GY���YQYuc�����"5q��i�8��)f����	������
�^�4��B8*>����W��/,�GaL)8_���$�at5�%d����H��i��oW��z::L��{z��^Q����h�
u��z�n7R�P/c0GY��G���N9�f�{���j�Gpm��Z]����9b~��YD0tt;��Q���D��J_DR����w�M���a���c�f�}�I�qr��!��0���"Q�m�w�
���q��E�\�avf���_��::L ��3���o�E�Cw���z������^2J��|��|��@�/�M��#�mu?�#�c�_NX�i�����8N��/�YYQ�z�R���~r�[��=���L�RQ��|���,�r�
�H�PP��,id�!���+��/���5��LP��@G�y������M��2�|3�mW�H
�?gI�L���W$��&'O~MO��Lg�:�DQ�P��>L�8*k���6DG��=Lz�>�2��C%�v��j����:�2gO��"L���4��.���?��R��f5������D�A���=H���~F{�V��vz�$�AL�����ZK�V�TbN���sVQnQ-F�1�m�N`���V�� �Iz���g���Vua��%���CBq?
�������������a�1��-s$�Sp�D���������*��P��=\*r��	��8��:��|���x�E�s\��`9!u�%F�P?9*D(�/��dO"���:��v|���>�'v��)4���:@K�>���%��1+6jA���)p����^-��N���tr	�����p�.���o���������,���]��(fw��n$Q`�nA�5��U�e����t9����y�,�}jD�OT?����[�����ef<��3��N�������'� w��*���z9���W;�O�E5��<���Uv���K��
�y&��c���y� ~'3�{�-P:*j7��*�c\��O={Iq���&�����z ��:���I���fT�96�������w������������XC�`s;�sQ����r���:e�W�^�",,�\7��������������kJdd�a}��0[������L�������E��V[,���@/���j��Z��W�4GH��-�O��N�Z9{�/s��U�����-���n�Z9|��/���)�BH�$�.�v�����k@����b-JM�R��Bc�=�:%�F w�Gx;���e���-�bm�*�rd�v�]��m�#a�K��^���8.B;��k��!2�N�8�QZ
�g+
q��g���@��D$��3�����GpVU�4
�A����D���0%A��M}e�c�Z���5Xz$�~���A
.����b��L��
�D���y�k��>��:wB8�C�.@�/y������������R�MG<��g�����E�)�q;B]�l&%���Q�x� ��t�K�+W�����$aD�8���n��y|�}��-�X7�}����k��B����/��=
�8CZ����JRbk��4�i�|z+��w��:�5�+���\C�<�H���5bN����ZIr���D�J���Y������6�5G�X�K�8��-��t�i���4F��(��_�h��!����q�)��E�����k�V|#u��8��;�]�#?�C�4����� �(��g��4��IA���~��)?N��-C���	��8��N��C��_�y�[1�v�`H�2�p�@mn0�S�����[�1���Z������5�r�R���q;B}��Z;����c�,�7����BtV3�����@��";%��j4!�����/���ZB�lJ����4C~V[^+��=-���D.J=)����N2�����1���uO�5�Xe�.�Yz#��0PJ�����X�N,�.����L���o��x��{85p�|V=|�htgJ$����tp��A�bnF�M�����_�_��_���^��k~��D��p��`�����y}@jxxb0"&���3�i�S$3+jR�)A�f�����B�6����zG������^m{��l9�'��i�]:��KZ{�g�mOk�������8�.\"�(/m�DB�i���`���)��>��KT}.V}�K�b	�q�l����%�&4���L�$��J�d�f���������SZ������ .�(>g'�v>��@�������-Y�������z��XD���mJ����(������u[����;Y�c��	����<�G)Y{���-]_T��gM�j�u'�+�	�)�G2�G�,eQ!F<'�K$���l'�e��Ul����(h���]�d���,�����%��d����p���va{w��28J�5	]<�A��������d��VI��!.��6O
8vc����P��#�u��>Ml;5�Qi��������N@����R[��3K_��>DVv��td��.���e��B-�jOM��X��n�jYV�^�7D`���Ad��������aF�;��D_�<:�y�aj�&r&z����F�Gg0�0Lm���L�
S{�;�Q��Z����R,���NHf�`B;A��h�dd	&��S�$���NDf�`";
�A�ih�$T/Fg�7��%��S�'��;'m��d/@.%��+��Q��B,�,>�;u�71��W�,�3�z$2H&��	��6�x��h��p�H��8�w3��������0�3�dy���8�nE�i#�����<Q^�C1o����)�(1/'�f{dG������n#����������|f}�Y������a�����3�'%C��f����*;����`T�r���P�'���?y��|N�������Qn��R\}�Z5�����I��YPx��!9�FF��o�V�����f8��O�(h��B�rQ'�8�IQ���Vl�<�"��4�m������"I}J�|��i��TE�N7��Z35 A����� ��|#wx?�{��P@�!$ �VW�u��S�x$�r�T!q�}]�]t$a��D� Q�W�������D����D�O����.�����	>|�����0�����Y�rF��)c(���"��1?������J�Nyln+2+�h+Pdh����nD��#���/�����Hi<x�$%���$f�x&q�G�X�<�*������!f�s��b�
f������?t�\��?T��o�S����|�����:2��8���H�W���%�L>_��WI�?}q�
� �������+������-5�{����f�U�G�,��K��\s�Ql��.��5H2d'�o_2+*b4�S^g��\^i"�(����A��l��i�A��<�n9���C�ND����g]
=7�\q��l�:��8t�N[9}^(.6d28<�.�wb��=���a���#b4^�,J�������X�>�?<�A��G�6�{;^�5Aw����E�^��lDq��L�8���6����6iR>�����X�O�[����rj�P%��� �U������*�L��w�*�z��M�'�TX�gS�l����S���Cv�p�=�������,��$������_�,|����������Jg*	��5Ee��-�R�P)��B���Q�J���A~�l���:����
8�"�c��]r:��������
��;��Bj�p����%��'�)������e�M���x�m�s���������~O���4���	$�`�t��UM �&p�
(�n	(X8�]�u'=X:�]�u,������s�����R��i�j��d�������&��F�Mf�hn=3�a��i��d�������&�����u
�����{����c� l>m�8�Y��u�=���g�1�n fph�1�;�#tS�@��(�c^t0cl���g����8�U���1��8���0������4O�v�r�1)�9�tf��L�(������,gFL�r�r�1)�9�pf���*���� c{��`=b�GQ=(��wP8QI�|��� ��r�����c���u��P+��N�Z�����.n�z��Cp�r������X�dq��v�
�v(�OD�
�0���Q�~�o�|����`�3��t%���=���k���.*]�]�,�p�O)��]�hO�������.a�2��4N���_��n��W~E�-
w�f/�}P�C�2K]�z��_�
�yYrIw�)�#���~�g����J���l
���&+5��7�������8f������/�2���������a��u��>���C8�fP*w��c[9�5<Q�H���'�3o��0;��?�5��������O��3�yoa�O���%�o��&�5g��qH�g��;'�D�Di�GI��k+�8�����u����gE��0�Z��M���jv���U��������������}<�������&�cp�MZSu�o��
���lm��%��>����i�e�������^,�aw�y�AdtC����v}>�*��j�F���K r�������}=�D`N���� ��v�&�SR�\���P�����v�f���)i��
��������Q�Gg�|���c�\��9j�� ���]U	1��{����WE3�w/�r�s�vl/�(���v�g�����n�-�O���
��`�J�'q�4"��a����J����/�P]n�����Rc����U�gX#\���n)��Au�x�$u^���$��7���.��=��sS��
�{&*k��I1���Cko1��l�=Y��[�e���r�y�,��c��,���-�{�/+��,&�i6s�C��}mu�%���n)�O��}�fA/'�Ge��%�;vP�i��z��\/.��tT]�.6o���A�!?,�X�N��������leK[����bu��vn{�.�}Xz�.�!`���$8��QZ9wQ,���=~����t�����,R3^��C�8.@+��j���!\E�Z0��9(R'8���a�J3@��J3`@�5qP����q�:�'<,���"�4������-�c����^��.����\!���.�+����86����c��U�Y�;�����&+����7.:���b���|��p�f9��m�l�1	�v@#(��hr�o2O����������gU�t����z��{\�'���x:8���z������a~��=���|m�<�r|�VA���D����;�zhw�p�,�K|e}�~����6�O^Yg������P��oy��,�N�l�.m0��v�;q4����F�6D��P��_~)�[���q�E	L;�_����
{V��v�v���1�����Iz�����*S(Vm�38���v�c�%������j��.Y��B���p�����4����K&v���}�)
��a]L�.���#���X�-t�	����C��df�����p���6s83��cn�bF��b�bf�����a�4>s33�Ea.�`f��������~����/G�/3\p��d�e@������3�t���(bb��L�o/��E-�Y�='�����?y_Y�>�{�f���sq�/����IW���1��u��0�`�*D�@������h�-R�������0#���F������.�9���\�d��V;����[�d�-�Yk�5|-�^<]k��������6B;�Y��no�X���H�7��b�7����IhSc�(3@�E������4vk2Z��m�������������e��9I{O9m��;����AJ���=���a�����YD�����v:��,�	�s��"�I�S��.�1 ����T���w�c���'�=u�U���0�jA�6��VU`z	��by���0��`.�1�������\�������U!�=�CK!������Y&s������F4�N�qE�qx����]���}#=�����`'�u���'�G�D��cm�*c���8��^Q��:�$�
�+��0��`A�|��<
����z
���>c��Y�IZ�679+�|Q,�q�� Bn���y�[7�����"�&��]���"�_� �����WW����f���f�H31����m[E��n�HX��\>����?�U�iE�`mIt��pwD���
i����y�
�*�[����C��ua�$��F|��,�1���-Q9O���}������b�2�f����6�ln*���\N����'�����h�$�
�3�7��_����@��=H!q���L�D������)���n)$'yv}D-$����ETjW�o*��'>E
N�[���
�Y��=��[�-��Dd��p2�j}����I�7*��u��4�E��:h����~8Tm��s�����g�R8VH���,����S���M2��t��Q��p�%:�1���z�
�:�� \W`K����|���~���.�A��R��Rx��
:�(ZWX��������.W��v���uc���t�tx7<�Q�����/�Y�Cr��K�]�������q��l3t�`p���B[���f�����.!gm��0Q�ec�k���2���G��@�M�6��W$���d��=
����W��E�x��@O����������)��|�%��A����~g���/,H�NI��<����M��ML*�[��W�w�K��x]�����T�������$�%��2�33�k����N�v�.>���P���V5K��1������K���';�
$
N�2�'��p����l�/Nq���9�����H]�����_��e�W�`�����IXo:�r��0�8N����="aQ�F�~
"�.������k�!S�������!@&=�	e�G��7%:Ty��g%�U�|��fgq��K��(���K|��\�������{�`�zc}:��w�$$	�;��:?��o�1��Y|����y� �wN�p=����6Y�$IA�S�C��1�C|7ef�K�y�!Ti��������J��0�YJ�<0�t�2���`q?g
�Bec��YT��3�S.����oF�{�����W�����8����B{M�I��,t���t%e\�u��n�,5�n�'�b��8+�f�t���Q��{��z�uI;g�����f�
���5i�`�k��/�oT�&Kw�[������)�m�t�5M����;�}7�����Lo��1��x��)8m0w?�5�E���9�r�S=�U�i��}i�E��ZEt��5z`1�zA��������kS���5��)650v;1Fu��������!��+�Q-�sz��);7x�����8��`>bCQ�8�YN�Ud?'��G3m��~d�n�Z���i{�������m����-�{���pu;r��E�"3p��^�-;����oTp(�G��������>����<�	�c�����>?dx2p|BD�;����?�:�,�RvNY������q}Y(����0>
U{+q�T3�s}� O�3[]��W���uu�4���=PR2tw��mX���7�z�0�5
w���%y�i���r�c������G&j\��W��+l$}�.E�����:��\5]}���2�>MW����|6��E�2|�K��7a���{
O'�9�-��+{��������sQw6a�0��������?��u�
^����	������Q������d�*�N�VGH{
b�0�C���o��(So-���wdy�\��L�������
�2��O���lU��l�4��D����<��
�st9�8��{�y���D�rV��O?�O���'D���e��4�yy��j���.����h�A�g�{�����cQ�Y�u�G-����]���o�/��K��,����i���c?����L�\��y��O����x��K�:R�X�r�"�h�@2��+�K�"�w/����&��D�BkeU����?�*q��|[�Q�w�������<���jl��s���*�pO�YP��c��yC�� 0y��kw����We�{��ir�p����0�m�e4N��=�"	�}}������j�.L�>3��W�1������~m�*#R|���s���_}�4����������C�r�n�����?�,nW������_�+!�e���r�':Ch<�D��x�O��/|�PN�l���������� �t=���G�T?:���h	X��k��Uv�*���ix<���q�U9�������5�3������.����4�Y���5��J����W�-�d*�/�P��Y����e�g����=y�Xp��3���{��Z3���;*������p�����`�/A������[]���� ��(�&	E���E��M�����Ed�r����)��}�0�������){K���I����U(�*uT�?��mt�Bqy/6������4���V\��e�_���a.r������r�-�� ��!D����]ba�^���%m�q�'2�.�wcN���/Zzf���v���\{#�����)���a�.���������9
�����_�6z����y�q�=3����.���I�	�,����i���9���%�K��Y�������xL�1�{�����P������/�}����� m���gk�;n�v���P.��AP�b�����<*d6��.Q��
.�������$�R�h�v��*s��.�G�ZT��Cq�o��}(�>����uC�����6�U[�����f,^o�Y���w���w�������{Y���d�h9�>P���S�����J���q��t���A!7
)V2z!jF�� 8��mr���)t���K�j&��I��������m$������q�%�d��f�b�]n��ak8���E��!����4��k����T�d�S���
!��mhL��hEkC����(�QQ�zH���b�������f������TTu5rC �Fn#.u�c�s$���1\�%�r+~{,"��2�4e�a\��G�L�1d��In��9	r*6�� ���iD�����R6�I�F���a��N�n�C�G�Vu7^�bEo^��c�2�;�A�����n`�kc%B�U3�&�n���8f��&��I�����%�v���/.��V(g���d��	�@q�.�s'��P(B	~�1��\[��RC}M�:6P��Vk�/�$a����~1E[��G�� ���������L�3�W�[��w��LC#d���C(ta�4y�o*��H���	������B��?=���f��
9�]K�����<��Zz������U�Me���?��`���a1g*5��#���VL�w��\�����i3��oD�%
�I���6���S�`���AKj�@��y
zs�=��,2�'�!X?�(M^�*��s���v��Fj;w��Q�"��<�����|�/�c�Y5��;�!���_�3r�R,/�8�M�r�����<�V*ff��ec��^���.&���!6o�r�����&}v�4
>��q�e���=�Q���yd�Q�D�.P�;�{$9���+�_Xp�������E�{Gz�$Gw}�}��{/}7����]>�Q��M�z����we�P~D�����,��6F�������c�d�[W���%�?��mZ����G�"�9��.
���9������P�^����~��� n�����-�?zP�"���g"K1�;9a��]����!��4|���<%�|�8"��H=��M!;E�P� ~���6��L���c��z��3�3�b��uI�~����)8%����qR��{ag��e*K:�U�q�&A��p
�Q*�+����Bu�;�3O�N�����z{�[�"�O����a��A/5��x#y��������)\��&���y��Lf��u�k������N^��%e^����q������&Gy����wQ�AV'��q�SY�|C�����2q��u�I��;�LYc�.�A�j�,H����8S����������F�!�\UJ������4(��H=�RU�?3�t����8+3�5��7]R�
�����wpLe��^�FK��������ae�����;�����]�5���.���ts�������cZ����i�'���:pb��vm�qY���,�����$n
9��i<���cN���ifAi�SqN�.�Mw���^7�� �Y/Jhw�-e>���4(�@.aR��*>�'�j���!wvf;���%��_����ugU�$�U������&zp%�}��/��k�9���&o����$o��@��W�UMwS�����*�����e��xI�)��d���>�s!�
�`t��M����
E1�"S|i�R�/����w��N����D{@�,5��f��\���7�{I�����:�)���p��J�adt�95$W��N>���y�����QN�����|.{�5c����/��;�'����$&� N�7k�f���#�TN��Wy;�<��0N�����x���"�me��������Y���"�/���kKoX��_� 
��v��-y���d2�������������Q�V���}��9���
^{Y�U�������/��s��=�o1)�.��;a�V,�;�����82U��x�"~i:���������U�X|4�"�I$.�Q����z�}J-�{���}d9;�Mz�7��������X���5#�l-GU]�Z����'e���tO�s���wJ��%g�[uX P\����f�U�#K��I�a3�qym7��q5k:s��v��I[�{�]Z�.dM�yqa��MX]",�Y�hp��|�SNLR�O��d���=�%�(�����S����8���e>����In��r���Y�E�g5~�'��O\
������s��"�k�P_�����(g�~������| x]�/MV�������W=H{t@�7�Lll�)�c@
��U�t����SW�|�e�t~Y���s��~9��E��/�'����_�������.az=�Wd��I���'�	�����h�]l�4
3�S��5�y��)x�3?��c|�Cy��s�1�9Jv��,��+!~�D-
>#9���$����8������6~��c��~+����r�3�u�c�r������'������n��[h��_��z*�6)��,�Lv9��k�!�,��I�y$�i��!��.&f�w��u
b�O8�fZV�n�-yy1���/3]E���X��������nk�5�`�$��B��G>�i���m���8L}��\�)�}�b,v	����x�b���H�5��t)�$�x�(���g�����(Zj1U�����7�$U��Y.�����J�L���SQ���/QH��f����2���$��ZP����KX�<�F�b41��F<&.�����{I���E��rQ3�}���N�@�#y$��-���z��?�SS��h����D�~,�Jy7����r]5��v�G�~�es�,�/�����4H��m�����>J��_�V���W���=�<�K){SJ�?�����US���o�}��f�����������`Q�f
#58Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Peter Eisentraut (#57)
2 attachment(s)
prokind column (was Re: [HACKERS] SQL procedures)

On 1/9/18 13:08, Peter Eisentraut wrote:

On 1/2/18 17:47, Peter Eisentraut wrote:

On 1/2/18 11:47, Tom Lane wrote:

+1 --- seems like a new bool column is the thing. Or may we should merge
"proisprocedure" with proisagg and proiswindow into an enum prokind?
Although that would break some existing client-side code.

prokind sounds good. I'll look into that.

Here is a patch set for that. (I just kept the pg_proc.h changes
separate for easier viewing.) It's not quite complete; some frontend
code still needs adjusting; but the idea is clear.

I think this would be a pretty good change. It doesn't appear to save a
ton amount of code, although a couple of cases where inconsistent
settings of proisagg and proiswindow had to be handed could be removed.
Because window functions are a separate kind in pg_proc but not in the
object address system, inconsistencies will remain in the system, but I
guess that's just the way it is.

Here is this patch updated. The client changes are now complete and all
the tests pass. I have also rolled back the places where the code used
prorettype to detect procedures and replaced this by the new facility.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

v2-0001-Add-prokind-column-replacing-proisagg-and-proiswi.patchtext/plain; charset=UTF-8; name=v2-0001-Add-prokind-column-replacing-proisagg-and-proiswi.patch; x-mac-creator=0; x-mac-type=0Download
From 7e21a05d3f43f8d29f4592fa1d547bde9378c1fa Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Wed, 3 Jan 2018 18:21:20 -0500
Subject: [PATCH v2 1/2] Add prokind column, replacing proisagg and proiswindow

The new column distinguishes normal functions, procedures, aggregates,
and window functions.  This replaces the existing columns proisagg and
proiswindow, and replaces the convention that procedures are indicated
by prorettype == 0.
---
 doc/src/sgml/catalogs.sgml                      | 29 ++++++------
 src/backend/catalog/aclchk.c                    | 18 +++----
 src/backend/catalog/information_schema.sql      |  4 +-
 src/backend/catalog/objectaddress.c             |  6 +--
 src/backend/catalog/pg_aggregate.c              |  3 +-
 src/backend/catalog/pg_proc.c                   | 49 +++++++------------
 src/backend/catalog/system_views.sql            |  8 ++--
 src/backend/commands/functioncmds.c             | 29 ++++--------
 src/backend/commands/proclang.c                 |  9 ++--
 src/backend/commands/typecmds.c                 |  3 +-
 src/backend/parser/parse_coerce.c               |  3 +-
 src/backend/parser/parse_func.c                 | 27 +++++++----
 src/backend/utils/adt/ruleutils.c               | 10 ++--
 src/backend/utils/cache/lsyscache.c             |  4 +-
 src/bin/pg_dump/pg_dump.c                       | 17 +++++--
 src/bin/pg_dump/t/002_pg_dump.pl                |  6 +--
 src/bin/psql/describe.c                         | 62 +++++++++++++++++++------
 src/bin/psql/tab-complete.c                     |  6 +--
 src/include/catalog/pg_class.h                  |  2 +-
 src/include/catalog/pg_proc.h                   | 54 +++++++++++----------
 src/include/catalog/pg_proc_fn.h                |  3 +-
 src/pl/plpython/plpy_procedure.c                |  2 +-
 src/pl/tcl/pltcl.c                              |  2 +-
 src/test/regress/expected/alter_generic.out     |  2 +-
 src/test/regress/expected/create_function_3.out |  9 ++++
 src/test/regress/expected/opr_sanity.out        | 38 +++++++--------
 src/test/regress/expected/rules.out             |  8 ++--
 src/test/regress/sql/alter_generic.sql          |  2 +-
 src/test/regress/sql/create_function_3.sql      |  8 ++++
 src/test/regress/sql/opr_sanity.sql             | 38 +++++++--------
 30 files changed, 252 insertions(+), 209 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 71e20f2740..19a40b9992 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5062,15 +5062,17 @@ <title><structname>pg_proc</structname></title>
   </indexterm>
 
   <para>
-   The catalog <structname>pg_proc</structname> stores information about functions (or procedures).
-   See <xref linkend="sql-createfunction"/>
-   and <xref linkend="xfunc"/> for more information.
+   The catalog <structname>pg_proc</structname> stores information about
+   functions, procedures, aggregate functions, and window functions
+   (collectively also known as routines).  See <xref
+   linkend="sql-createfunction"/>, <xref linkend="sql-createprocedure"/>, and
+   <xref linkend="xfunc"/> for more information.
   </para>
 
   <para>
-   The table contains data for aggregate functions as well as plain functions.
-   If <structfield>proisagg</structfield> is true, there should be a matching
-   row in <structfield>pg_aggregate</structfield>.
+   If <structfield>prokind</structfield> indicates that the entry is for an
+   aggregate function, there should be a matching row in
+   <structfield>pg_aggregate</structfield>.
   </para>
 
   <table>
@@ -5157,17 +5159,12 @@ <title><structname>pg_proc</structname> Columns</title>
      </row>
 
      <row>
-      <entry><structfield>proisagg</structfield></entry>
+      <entry><structfield>prokind</structfield></entry>
       <entry><type>bool</type></entry>
       <entry></entry>
-      <entry>Function is an aggregate function</entry>
-     </row>
-
-     <row>
-      <entry><structfield>proiswindow</structfield></entry>
-      <entry><type>bool</type></entry>
-      <entry></entry>
-      <entry>Function is a window function</entry>
+      <entry><literal>f</literal> for a normal function, <literal>p</literal>
+      for a procedure, <literal>a</literal> for an aggregate function,
+      <literal>w</literal> for a window function</entry>
      </row>
 
      <row>
@@ -5264,7 +5261,7 @@ <title><structname>pg_proc</structname> Columns</title>
       <entry><structfield>prorettype</structfield></entry>
       <entry><type>oid</type></entry>
       <entry><literal><link linkend="catalog-pg-type"><structname>pg_type</structname></link>.oid</literal></entry>
-      <entry>Data type of the return value, or null for a procedure</entry>
+      <entry>Data type of the return value</entry>
      </row>
 
      <row>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 1156627b9e..3f2c629c47 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -830,21 +830,17 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames)
 								BTEqualStrategyNumber, F_OIDEQ,
 								ObjectIdGetDatum(namespaceId));
 
-					/*
-					 * When looking for functions, check for return type <>0.
-					 * When looking for procedures, check for return type ==0.
-					 * When looking for routines, don't check the return type.
-					 */
 					if (objtype == OBJECT_FUNCTION)
+						/* includes aggregates and window functions */
 						ScanKeyInit(&key[keycount++],
-									Anum_pg_proc_prorettype,
-									BTEqualStrategyNumber, F_OIDNE,
-									InvalidOid);
+									Anum_pg_proc_prokind,
+									BTEqualStrategyNumber, F_CHARNE,
+									CharGetDatum(PROKIND_PROCEDURE));
 					else if (objtype == OBJECT_PROCEDURE)
 						ScanKeyInit(&key[keycount++],
-									Anum_pg_proc_prorettype,
-									BTEqualStrategyNumber, F_OIDEQ,
-									InvalidOid);
+									Anum_pg_proc_prokind,
+									BTEqualStrategyNumber, F_CHAREQ,
+									CharGetDatum(PROKIND_PROCEDURE));
 
 					rel = heap_open(ProcedureRelationId, AccessShareLock);
 					scan = heap_beginscan_catalog(rel, keycount, key);
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 686528c354..aab9956335 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1413,7 +1413,7 @@ CREATE VIEW routines AS
            CAST(current_database() AS sql_identifier) AS routine_catalog,
            CAST(n.nspname AS sql_identifier) AS routine_schema,
            CAST(p.proname AS sql_identifier) AS routine_name,
-           CAST(CASE WHEN p.prorettype <> 0 THEN 'FUNCTION' ELSE 'PROCEDURE' END
+           CAST(CASE p.prokind WHEN 'f' THEN 'FUNCTION' WHEN 'p' THEN 'PROCEDURE' END
              AS character_data) AS routine_type,
            CAST(null AS sql_identifier) AS module_catalog,
            CAST(null AS sql_identifier) AS module_schema,
@@ -1464,7 +1464,7 @@ CREATE VIEW routines AS
            CAST('GENERAL' AS character_data) AS parameter_style,
            CAST(CASE WHEN p.provolatile = 'i' THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_deterministic,
            CAST('MODIFIES' AS character_data) AS sql_data_access,
-           CAST(CASE WHEN p.prorettype <> 0 THEN
+           CAST(CASE WHEN p.prokind <> 'p' THEN
              CASE WHEN p.proisstrict THEN 'YES' ELSE 'NO' END END AS yes_or_no) AS is_null_call,
            CAST(null AS character_data) AS sql_path,
            CAST('YES' AS yes_or_no) AS schema_level_routine,
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 80f561df1c..4e7817712d 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -4047,11 +4047,11 @@ getProcedureTypeDescription(StringInfo buffer, Oid procid)
 		elog(ERROR, "cache lookup failed for procedure %u", procid);
 	procForm = (Form_pg_proc) GETSTRUCT(procTup);
 
-	if (procForm->proisagg)
+	if (procForm->prokind == PROKIND_AGGREGATE)
 		appendStringInfoString(buffer, "aggregate");
-	else if (procForm->prorettype == InvalidOid)
+	else if (procForm->prokind == PROKIND_PROCEDURE)
 		appendStringInfoString(buffer, "procedure");
-	else
+	else /* function or window function */
 		appendStringInfoString(buffer, "function");
 
 	ReleaseSysCache(procTup);
diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c
index f14ea26fcb..50d8d81f2c 100644
--- a/src/backend/catalog/pg_aggregate.c
+++ b/src/backend/catalog/pg_aggregate.c
@@ -616,8 +616,7 @@ AggregateCreate(const char *aggName,
 							 InvalidOid,	/* no validator */
 							 "aggregate_dummy", /* placeholder proc */
 							 NULL,	/* probin */
-							 true,	/* isAgg */
-							 false, /* isWindowFunc */
+							 PROKIND_AGGREGATE,
 							 false, /* security invoker (currently not
 									 * definable for agg) */
 							 false, /* isLeakProof */
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index b59fadbf76..491eee84c3 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -74,8 +74,7 @@ ProcedureCreate(const char *procedureName,
 				Oid languageValidator,
 				const char *prosrc,
 				const char *probin,
-				bool isAgg,
-				bool isWindowFunc,
+				char prokind,
 				bool security_definer,
 				bool isLeakProof,
 				bool isStrict,
@@ -335,8 +334,7 @@ ProcedureCreate(const char *procedureName,
 	values[Anum_pg_proc_prorows - 1] = Float4GetDatum(prorows);
 	values[Anum_pg_proc_provariadic - 1] = ObjectIdGetDatum(variadicType);
 	values[Anum_pg_proc_protransform - 1] = ObjectIdGetDatum(InvalidOid);
-	values[Anum_pg_proc_proisagg - 1] = BoolGetDatum(isAgg);
-	values[Anum_pg_proc_proiswindow - 1] = BoolGetDatum(isWindowFunc);
+	values[Anum_pg_proc_prokind - 1] = CharGetDatum(prokind);
 	values[Anum_pg_proc_prosecdef - 1] = BoolGetDatum(security_definer);
 	values[Anum_pg_proc_proleakproof - 1] = BoolGetDatum(isLeakProof);
 	values[Anum_pg_proc_proisstrict - 1] = BoolGetDatum(isStrict);
@@ -403,6 +401,21 @@ ProcedureCreate(const char *procedureName,
 			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
 						   procedureName);
 
+		/* Can't change aggregate or window-function status, either */
+		if (oldproc->prokind != prokind)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot change routine type"),
+					 (oldproc->prokind == PROKIND_AGGREGATE ?
+					  errdetail("\%s\" is an aggregate function.", procedureName) :
+					  oldproc->prokind == PROKIND_FUNCTION ?
+					  errdetail("\%s\" is a function.", procedureName) :
+					  oldproc->prokind == PROKIND_PROCEDURE ?
+					  errdetail("\%s\" is a procedure.", procedureName) :
+					  oldproc->prokind == PROKIND_WINDOW ?
+					  errdetail("\%s\" is a window function.", procedureName) :
+					  0)));
+
 		/*
 		 * Not okay to change the return type of the existing proc, since
 		 * existing rules, views, etc may depend on the return type.
@@ -535,34 +548,6 @@ ProcedureCreate(const char *procedureName,
 			}
 		}
 
-		/* Can't change aggregate or window-function status, either */
-		if (oldproc->proisagg != isAgg)
-		{
-			if (oldproc->proisagg)
-				ereport(ERROR,
-						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("function \"%s\" is an aggregate function",
-								procedureName)));
-			else
-				ereport(ERROR,
-						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("function \"%s\" is not an aggregate function",
-								procedureName)));
-		}
-		if (oldproc->proiswindow != isWindowFunc)
-		{
-			if (oldproc->proiswindow)
-				ereport(ERROR,
-						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("function \"%s\" is a window function",
-								procedureName)));
-			else
-				ereport(ERROR,
-						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("function \"%s\" is not a window function",
-								procedureName)));
-		}
-
 		/*
 		 * Do not change existing ownership or permissions, either.  Note
 		 * dependency-update code below has to agree with this decision.
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 5652e9ee6d..5e6e8a64f6 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -332,9 +332,11 @@ CREATE VIEW pg_seclabels AS
 UNION ALL
 SELECT
 	l.objoid, l.classoid, l.objsubid,
-	CASE WHEN pro.proisagg = true THEN 'aggregate'::text
-	     WHEN pro.proisagg = false THEN 'function'::text
-	END AS objtype,
+	CASE pro.prokind
+            WHEN 'a' THEN 'aggregate'::text
+            WHEN 'f' THEN 'function'::text
+            WHEN 'p' THEN 'procedure'::text
+            WHEN 'w' THEN 'window'::text END AS objtype,
 	pro.pronamespace AS objnamespace,
 	CASE WHEN pg_function_is_visible(pro.oid)
 	     THEN quote_ident(pro.proname)
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index abdfa249c0..29380f61c3 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -1097,8 +1097,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
 						   languageValidator,
 						   prosrc_str,	/* converted to text later */
 						   probin_str,	/* converted to text later */
-						   false,	/* not an aggregate */
-						   isWindowFunc,
+						   stmt->is_procedure ? PROKIND_PROCEDURE : (isWindowFunc ? PROKIND_WINDOW : PROKIND_FUNCTION),
 						   security,
 						   isLeakProof,
 						   isStrict,
@@ -1126,7 +1125,7 @@ RemoveFunctionById(Oid funcOid)
 {
 	Relation	relation;
 	HeapTuple	tup;
-	bool		isagg;
+	char		prokind;
 
 	/*
 	 * Delete the pg_proc tuple.
@@ -1137,7 +1136,7 @@ RemoveFunctionById(Oid funcOid)
 	if (!HeapTupleIsValid(tup)) /* should not happen */
 		elog(ERROR, "cache lookup failed for function %u", funcOid);
 
-	isagg = ((Form_pg_proc) GETSTRUCT(tup))->proisagg;
+	prokind = ((Form_pg_proc) GETSTRUCT(tup))->prokind;
 
 	CatalogTupleDelete(relation, &tup->t_self);
 
@@ -1148,7 +1147,7 @@ RemoveFunctionById(Oid funcOid)
 	/*
 	 * If there's a pg_aggregate tuple, delete that too.
 	 */
-	if (isagg)
+	if (prokind == PROKIND_AGGREGATE)
 	{
 		relation = heap_open(AggregateRelationId, RowExclusiveLock);
 
@@ -1203,13 +1202,13 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
 		aclcheck_error(ACLCHECK_NOT_OWNER, stmt->objtype,
 					   NameListToString(stmt->func->objname));
 
-	if (procForm->proisagg)
+	if (procForm->prokind == PROKIND_AGGREGATE)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is an aggregate function",
 						NameListToString(stmt->func->objname))));
 
-	is_procedure = (procForm->prorettype == InvalidOid);
+	is_procedure = (procForm->prokind == PROKIND_PROCEDURE);
 
 	/* Examine requested actions. */
 	foreach(l, stmt->actions)
@@ -1525,14 +1524,10 @@ CreateCast(CreateCastStmt *stmt)
 					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 					 errmsg("cast function must not be volatile")));
 #endif
-		if (procstruct->proisagg)
+		if (procstruct->prokind != PROKIND_FUNCTION)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-					 errmsg("cast function must not be an aggregate function")));
-		if (procstruct->proiswindow)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-					 errmsg("cast function must not be a window function")));
+					 errmsg("cast function must be a normal function")));
 		if (procstruct->proretset)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -1777,14 +1772,10 @@ check_transform_function(Form_pg_proc procstruct)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 				 errmsg("transform function must not be volatile")));
-	if (procstruct->proisagg)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-				 errmsg("transform function must not be an aggregate function")));
-	if (procstruct->proiswindow)
+	if (procstruct->prokind != PROKIND_FUNCTION)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-				 errmsg("transform function must not be a window function")));
+				 errmsg("transform function must be a normal function")));
 	if (procstruct->proretset)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/proclang.c b/src/backend/commands/proclang.c
index 2ec96242ae..447bd49f89 100644
--- a/src/backend/commands/proclang.c
+++ b/src/backend/commands/proclang.c
@@ -129,8 +129,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt)
 									  F_FMGR_C_VALIDATOR,
 									  pltemplate->tmplhandler,
 									  pltemplate->tmpllibrary,
-									  false,	/* isAgg */
-									  false,	/* isWindowFunc */
+									  PROKIND_FUNCTION,
 									  false,	/* security_definer */
 									  false,	/* isLeakProof */
 									  false,	/* isStrict */
@@ -169,8 +168,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt)
 										  F_FMGR_C_VALIDATOR,
 										  pltemplate->tmplinline,
 										  pltemplate->tmpllibrary,
-										  false,	/* isAgg */
-										  false,	/* isWindowFunc */
+										  PROKIND_FUNCTION,
 										  false,	/* security_definer */
 										  false,	/* isLeakProof */
 										  true, /* isStrict */
@@ -212,8 +210,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt)
 										  F_FMGR_C_VALIDATOR,
 										  pltemplate->tmplvalidator,
 										  pltemplate->tmpllibrary,
-										  false,	/* isAgg */
-										  false,	/* isWindowFunc */
+										  PROKIND_FUNCTION,
 										  false,	/* security_definer */
 										  false,	/* isLeakProof */
 										  true, /* isStrict */
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 899a5c4cd4..bf3cd3a454 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1672,8 +1672,7 @@ makeRangeConstructors(const char *name, Oid namespace,
 								 F_FMGR_INTERNAL_VALIDATOR, /* language validator */
 								 prosrc[i], /* prosrc */
 								 NULL,	/* probin */
-								 false, /* isAgg */
-								 false, /* isWindowFunc */
+								 PROKIND_FUNCTION,
 								 false, /* security_definer */
 								 false, /* leakproof */
 								 false, /* isStrict */
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 085aa8766c..665d3327a0 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -834,8 +834,7 @@ build_coercion_expression(Node *node,
 		 */
 		/* Assert(targetTypeId == procstruct->prorettype); */
 		Assert(!procstruct->proretset);
-		Assert(!procstruct->proisagg);
-		Assert(!procstruct->proiswindow);
+		Assert(procstruct->prokind == PROKIND_FUNCTION);
 		nargs = procstruct->pronargs;
 		Assert(nargs >= 1 && nargs <= 3);
 		/* Assert(procstruct->proargtypes.values[0] == exprType(node)); */
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 2a4ac09d5c..9dbf2c2b63 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -1614,14 +1614,25 @@ func_get_detail(List *funcname,
 				*argdefaults = defaults;
 			}
 		}
-		if (pform->proisagg)
-			result = FUNCDETAIL_AGGREGATE;
-		else if (pform->proiswindow)
-			result = FUNCDETAIL_WINDOWFUNC;
-		else if (pform->prorettype == InvalidOid)
-			result = FUNCDETAIL_PROCEDURE;
-		else
-			result = FUNCDETAIL_NORMAL;
+
+		switch (pform->prokind)
+		{
+			case PROKIND_AGGREGATE:
+				result = FUNCDETAIL_AGGREGATE;
+				break;
+			case PROKIND_FUNCTION:
+				result = FUNCDETAIL_NORMAL;
+				break;
+			case PROKIND_PROCEDURE:
+				result = FUNCDETAIL_PROCEDURE;
+				break;
+			case PROKIND_WINDOW:
+				result = FUNCDETAIL_WINDOWFUNC;
+				break;
+			default:
+				elog(ERROR, "unrecognized prokind: %c", pform->prokind);
+		}
+
 		ReleaseSysCache(ftup);
 		return result;
 	}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index ba9fab4582..b32f776450 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2468,12 +2468,12 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 	proc = (Form_pg_proc) GETSTRUCT(proctup);
 	name = NameStr(proc->proname);
 
-	if (proc->proisagg)
+	if (proc->prokind == PROKIND_AGGREGATE)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("\"%s\" is an aggregate function", name)));
 
-	isfunction = (proc->prorettype != InvalidOid);
+	isfunction = (proc->prokind == PROKIND_FUNCTION);
 
 	/*
 	 * We always qualify the function name, to ensure the right function gets
@@ -2500,7 +2500,7 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
 	/* Emit some miscellaneous options on one line */
 	oldlen = buf.len;
 
-	if (proc->proiswindow)
+	if (proc->prokind == PROKIND_WINDOW)
 		appendStringInfoString(&buf, " WINDOW");
 	switch (proc->provolatile)
 	{
@@ -2704,7 +2704,7 @@ pg_get_function_result(PG_FUNCTION_ARGS)
 	if (!HeapTupleIsValid(proctup))
 		PG_RETURN_NULL();
 
-	if (((Form_pg_proc) GETSTRUCT(proctup))->prorettype == InvalidOid)
+	if (((Form_pg_proc) GETSTRUCT(proctup))->prokind == PROKIND_PROCEDURE)
 	{
 		ReleaseSysCache(proctup);
 		PG_RETURN_NULL();
@@ -2804,7 +2804,7 @@ print_function_arguments(StringInfo buf, HeapTuple proctup,
 	}
 
 	/* Check for special treatment of ordered-set aggregates */
-	if (proc->proisagg)
+	if (proc->prokind == PROKIND_AGGREGATE)
 	{
 		HeapTuple	aggtup;
 		Form_pg_aggregate agg;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 51b6b4f7bb..161470aa34 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1601,7 +1601,7 @@ func_parallel(Oid funcid)
 
 /*
  * get_func_isagg
- *	   Given procedure id, return the function's proisagg field.
+ *	   Given procedure id, return whether the function is an aggregate.
  */
 bool
 get_func_isagg(Oid funcid)
@@ -1613,7 +1613,7 @@ get_func_isagg(Oid funcid)
 	if (!HeapTupleIsValid(tp))
 		elog(ERROR, "cache lookup failed for function %u", funcid);
 
-	result = ((Form_pg_proc) GETSTRUCT(tp))->proisagg;
+	result = ((Form_pg_proc) GETSTRUCT(tp))->prokind == PROKIND_AGGREGATE;
 	ReleaseSysCache(tp);
 	return result;
 }
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f7a079f0b1..a1e9a48ab3 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -5432,7 +5432,7 @@ getAggregates(Archive *fout, int *numAggs)
 						  "(p.oid = pip.objoid "
 						  "AND pip.classoid = 'pg_proc'::regclass "
 						  "AND pip.objsubid = 0) "
-						  "WHERE p.proisagg AND ("
+						  "WHERE %s AND ("
 						  "p.pronamespace != "
 						  "(SELECT oid FROM pg_namespace "
 						  "WHERE nspname = 'pg_catalog') OR "
@@ -5441,7 +5441,10 @@ getAggregates(Archive *fout, int *numAggs)
 						  acl_subquery->data,
 						  racl_subquery->data,
 						  initacl_subquery->data,
-						  initracl_subquery->data);
+						  initracl_subquery->data,
+						  (fout->remoteVersion >= 110000 ?
+						   "p.prokind = 'a'" :
+						   "p.proisagg"));
 		if (dopt->binary_upgrade)
 			appendPQExpBufferStr(query,
 								 " OR EXISTS(SELECT 1 FROM pg_depend WHERE "
@@ -5644,7 +5647,7 @@ getFuncs(Archive *fout, int *numFuncs)
 						  "(p.oid = pip.objoid "
 						  "AND pip.classoid = 'pg_proc'::regclass "
 						  "AND pip.objsubid = 0) "
-						  "WHERE NOT proisagg"
+						  "WHERE NOT %s"
 						  "\n  AND NOT EXISTS (SELECT 1 FROM pg_depend "
 						  "WHERE classid = 'pg_proc'::regclass AND "
 						  "objid = p.oid AND deptype = 'i')"
@@ -5664,6 +5667,9 @@ getFuncs(Archive *fout, int *numFuncs)
 						  initacl_subquery->data,
 						  initracl_subquery->data,
 						  username_subquery,
+						  (fout->remoteVersion >= 110000 ?
+						   "prokind = 'a'" :
+						   "proisagg"),
 						  g_last_builtin_oid,
 						  g_last_builtin_oid);
 		if (dopt->binary_upgrade)
@@ -11638,12 +11644,15 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
 						  "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
 						  "pg_catalog.pg_get_function_result(oid) AS funcresult, "
 						  "array_to_string(protrftypes, ' ') AS protrftypes, "
-						  "proiswindow, provolatile, proisstrict, prosecdef, "
+						  "%s, provolatile, proisstrict, prosecdef, "
 						  "proleakproof, proconfig, procost, prorows, "
 						  "proparallel, "
 						  "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
 						  "FROM pg_catalog.pg_proc "
 						  "WHERE oid = '%u'::pg_catalog.oid",
+						  (fout->remoteVersion >= 110000 ?
+						   "prokind = 'w' AS proiswindow" :
+						   "proiswindow"),
 						  finfo->dobj.catId.oid);
 	}
 	else if (fout->remoteVersion >= 90500)
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index ac9cfa04c1..0044e2e114 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -6257,8 +6257,7 @@
 						   prorows,
 						   provariadic,
 						   protransform,
-						   proisagg,
-						   proiswindow,
+						   prokind,
 						   prosecdef,
 						   proleakproof,
 						   proisstrict,
@@ -6290,8 +6289,7 @@
 		\QGRANT SELECT(prorows) ON TABLE pg_proc TO PUBLIC;\E\n.*
 		\QGRANT SELECT(provariadic) ON TABLE pg_proc TO PUBLIC;\E\n.*
 		\QGRANT SELECT(protransform) ON TABLE pg_proc TO PUBLIC;\E\n.*
-		\QGRANT SELECT(proisagg) ON TABLE pg_proc TO PUBLIC;\E\n.*
-		\QGRANT SELECT(proiswindow) ON TABLE pg_proc TO PUBLIC;\E\n.*
+		\QGRANT SELECT(prokind) ON TABLE pg_proc TO PUBLIC;\E\n.*
 		\QGRANT SELECT(prosecdef) ON TABLE pg_proc TO PUBLIC;\E\n.*
 		\QGRANT SELECT(proleakproof) ON TABLE pg_proc TO PUBLIC;\E\n.*
 		\QGRANT SELECT(proisstrict) ON TABLE pg_proc TO PUBLIC;\E\n.*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 466a78004b..0c3be1f504 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -100,12 +100,20 @@ describeAggregates(const char *pattern, bool verbose, bool showSystem)
 						  "  pg_catalog.format_type(p.proargtypes[0], NULL) AS \"%s\",\n",
 						  gettext_noop("Argument data types"));
 
-	appendPQExpBuffer(&buf,
-					  "  pg_catalog.obj_description(p.oid, 'pg_proc') as \"%s\"\n"
-					  "FROM pg_catalog.pg_proc p\n"
-					  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n"
-					  "WHERE p.proisagg\n",
-					  gettext_noop("Description"));
+	if (pset.sversion >= 110000)
+		appendPQExpBuffer(&buf,
+						  "  pg_catalog.obj_description(p.oid, 'pg_proc') as \"%s\"\n"
+						  "FROM pg_catalog.pg_proc p\n"
+						  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n"
+						  "WHERE p.prokind = 'a'\n",
+						  gettext_noop("Description"));
+	else
+		appendPQExpBuffer(&buf,
+						  "  pg_catalog.obj_description(p.oid, 'pg_proc') as \"%s\"\n"
+						  "FROM pg_catalog.pg_proc p\n"
+						  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n"
+						  "WHERE p.proisagg\n",
+						  gettext_noop("Description"));
 
 	if (!showSystem && !pattern)
 		appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
@@ -346,14 +354,31 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 					  gettext_noop("Schema"),
 					  gettext_noop("Name"));
 
-	if (pset.sversion >= 80400)
+	if (pset.sversion >= 110000)
+		appendPQExpBuffer(&buf,
+						  "  pg_catalog.pg_get_function_result(p.oid) as \"%s\",\n"
+						  "  pg_catalog.pg_get_function_arguments(p.oid) as \"%s\",\n"
+						  " CASE p.prokind\n"
+						  "  WHEN 'a' THEN '%s'\n"
+						  "  WHEN 'w' THEN '%s'\n"
+						  "  WHEN 'p' THEN '%s'\n"
+						  "  ELSE '%s'\n"
+						  " END as \"%s\"",
+						  gettext_noop("Result data type"),
+						  gettext_noop("Argument data types"),
+		/* translator: "agg" is short for "aggregate" */
+						  gettext_noop("agg"),
+						  gettext_noop("window"),
+						  gettext_noop("proc"),
+						  gettext_noop("func"),
+						  gettext_noop("Type"));
+	else if (pset.sversion >= 80400)
 		appendPQExpBuffer(&buf,
 						  "  pg_catalog.pg_get_function_result(p.oid) as \"%s\",\n"
 						  "  pg_catalog.pg_get_function_arguments(p.oid) as \"%s\",\n"
 						  " CASE\n"
 						  "  WHEN p.proisagg THEN '%s'\n"
 						  "  WHEN p.proiswindow THEN '%s'\n"
-						  "  WHEN p.prorettype = 0 THEN '%s'\n"
 						  "  WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN '%s'\n"
 						  "  ELSE '%s'\n"
 						  " END as \"%s\"",
@@ -362,7 +387,6 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* translator: "agg" is short for "aggregate" */
 						  gettext_noop("agg"),
 						  gettext_noop("window"),
-						  gettext_noop("proc"),
 						  gettext_noop("trigger"),
 						  gettext_noop("func"),
 						  gettext_noop("Type"));
@@ -494,7 +518,10 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 				appendPQExpBufferStr(&buf, "WHERE ");
 				have_where = true;
 			}
-			appendPQExpBufferStr(&buf, "NOT p.proisagg\n");
+			if (pset.sversion >= 110000)
+				appendPQExpBufferStr(&buf, "p.prokind <> 'a'\n");
+			else
+				appendPQExpBufferStr(&buf, "NOT p.proisagg\n");
 		}
 		if (!showTrigger)
 		{
@@ -516,7 +543,10 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 				appendPQExpBufferStr(&buf, "WHERE ");
 				have_where = true;
 			}
-			appendPQExpBufferStr(&buf, "NOT p.proiswindow\n");
+			if (pset.sversion >= 110000)
+				appendPQExpBufferStr(&buf, "p.prokind <> 'w'\n");
+			else
+				appendPQExpBufferStr(&buf, "NOT p.proiswindow\n");
 		}
 	}
 	else
@@ -528,7 +558,10 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		/* Note: at least one of these must be true ... */
 		if (showAggregate)
 		{
-			appendPQExpBufferStr(&buf, "p.proisagg\n");
+			if (pset.sversion >= 110000)
+				appendPQExpBufferStr(&buf, "p.prokind = 'a'\n");
+			else
+				appendPQExpBufferStr(&buf, "p.proisagg\n");
 			needs_or = true;
 		}
 		if (showTrigger)
@@ -543,7 +576,10 @@ describeFunctions(const char *functypes, const char *pattern, bool verbose, bool
 		{
 			if (needs_or)
 				appendPQExpBufferStr(&buf, "       OR ");
-			appendPQExpBufferStr(&buf, "p.proiswindow\n");
+			if (pset.sversion >= 110000)
+				appendPQExpBufferStr(&buf, "p.prokind = 'w'\n");
+			else
+				appendPQExpBufferStr(&buf, "p.proiswindow\n");
 			needs_or = true;
 		}
 		appendPQExpBufferStr(&buf, "      )\n");
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 8bc4a194a5..cf23d13d1e 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -349,7 +349,7 @@ static const SchemaQuery Query_for_list_of_aggregates = {
 	/* catname */
 	"pg_catalog.pg_proc p",
 	/* selcondition */
-	"p.proisagg",
+	"p.prokind = 'a'",
 	/* viscondition */
 	"pg_catalog.pg_function_is_visible(p.oid)",
 	/* namespace */
@@ -397,7 +397,7 @@ static const SchemaQuery Query_for_list_of_functions = {
 	/* catname */
 	"pg_catalog.pg_proc p",
 	/* selcondition */
-	"p.prorettype <> 0",
+	"p.prokind IN ('f', 'w')",
 	/* viscondition */
 	"pg_catalog.pg_function_is_visible(p.oid)",
 	/* namespace */
@@ -428,7 +428,7 @@ static const SchemaQuery Query_for_list_of_procedures = {
 	/* catname */
 	"pg_catalog.pg_proc p",
 	/* selcondition */
-	"p.prorettype = 0",
+	"p.prokind = 'p'",
 	/* viscondition */
 	"pg_catalog.pg_function_is_visible(p.oid)",
 	/* namespace */
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 26b1866c69..01cf59e7a3 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -151,7 +151,7 @@ DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t
 DESCR("");
 DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 22 0 f f f f f f f t n f 3 1 _null_ _null_ _null_));
 DESCR("");
-DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n f 3 1 _null_ _null_ _null_));
+DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f f t n f 3 1 _null_ _null_ _null_));
 DESCR("");
 DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 33 0 t f f f f f f t n f 3 1 _null_ _null_ _null_));
 DESCR("");
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 62e16514cc..fac91685c4 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -43,8 +43,7 @@ CATALOG(pg_proc,1255) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81) BKI_SCHEMA_MACRO
 	float4		prorows;		/* estimated # of rows out (if proretset) */
 	Oid			provariadic;	/* element type of variadic array, or 0 */
 	regproc		protransform;	/* transforms calls to it during planning */
-	bool		proisagg;		/* is it an aggregate? */
-	bool		proiswindow;	/* is it a window function? */
+	char		prokind;		/* see PROKIND_ categories below */
 	bool		prosecdef;		/* security definer */
 	bool		proleakproof;	/* is it a leak-proof function? */
 	bool		proisstrict;	/* strict with respect to NULLs? */
@@ -86,7 +85,7 @@ typedef FormData_pg_proc *Form_pg_proc;
  *		compiler constants for pg_proc
  * ----------------
  */
-#define Natts_pg_proc					29
+#define Natts_pg_proc					28
 #define Anum_pg_proc_proname			1
 #define Anum_pg_proc_pronamespace		2
 #define Anum_pg_proc_proowner			3
@@ -95,27 +94,26 @@ typedef FormData_pg_proc *Form_pg_proc;
 #define Anum_pg_proc_prorows			6
 #define Anum_pg_proc_provariadic		7
 #define Anum_pg_proc_protransform		8
-#define Anum_pg_proc_proisagg			9
-#define Anum_pg_proc_proiswindow		10
-#define Anum_pg_proc_prosecdef			11
-#define Anum_pg_proc_proleakproof		12
-#define Anum_pg_proc_proisstrict		13
-#define Anum_pg_proc_proretset			14
-#define Anum_pg_proc_provolatile		15
-#define Anum_pg_proc_proparallel		16
-#define Anum_pg_proc_pronargs			17
-#define Anum_pg_proc_pronargdefaults	18
-#define Anum_pg_proc_prorettype			19
-#define Anum_pg_proc_proargtypes		20
-#define Anum_pg_proc_proallargtypes		21
-#define Anum_pg_proc_proargmodes		22
-#define Anum_pg_proc_proargnames		23
-#define Anum_pg_proc_proargdefaults		24
-#define Anum_pg_proc_protrftypes		25
-#define Anum_pg_proc_prosrc				26
-#define Anum_pg_proc_probin				27
-#define Anum_pg_proc_proconfig			28
-#define Anum_pg_proc_proacl				29
+#define Anum_pg_proc_prokind			9
+#define Anum_pg_proc_prosecdef			10
+#define Anum_pg_proc_proleakproof		11
+#define Anum_pg_proc_proisstrict		12
+#define Anum_pg_proc_proretset			13
+#define Anum_pg_proc_provolatile		14
+#define Anum_pg_proc_proparallel		15
+#define Anum_pg_proc_pronargs			16
+#define Anum_pg_proc_pronargdefaults	17
+#define Anum_pg_proc_prorettype			18
+#define Anum_pg_proc_proargtypes		19
+#define Anum_pg_proc_proallargtypes		20
+#define Anum_pg_proc_proargmodes		21
+#define Anum_pg_proc_proargnames		22
+#define Anum_pg_proc_proargdefaults		23
+#define Anum_pg_proc_protrftypes		24
+#define Anum_pg_proc_prosrc				25
+#define Anum_pg_proc_probin				26
+#define Anum_pg_proc_proconfig			27
+#define Anum_pg_proc_proacl				28
 
 /* ----------------
  *		initial contents of pg_proc
@@ -5569,6 +5567,14 @@ DESCR("list of files in the WAL directory");
 DATA(insert OID = 5028 ( satisfies_hash_partition PGNSP PGUID 12 1 0 2276 0 f f f f f f i s 4 0 16 "26 23 23 2276" _null_ "{i,i,i,v}" _null_ _null_ _null_ satisfies_hash_partition _null_ _null_ _null_ ));
 DESCR("hash partition CHECK constraint");
 
+/*
+ * Symbolic values for prokind column
+ */
+#define PROKIND_FUNCTION 'f'
+#define PROKIND_AGGREGATE 'a'
+#define PROKIND_WINDOW 'w'
+#define PROKIND_PROCEDURE 'p'
+
 /*
  * Symbolic values for provolatile column: these indicate whether the result
  * of a function is dependent *only* on the values of its explicit arguments,
diff --git a/src/include/catalog/pg_proc_fn.h b/src/include/catalog/pg_proc_fn.h
index 098e2e6f07..b66871bc46 100644
--- a/src/include/catalog/pg_proc_fn.h
+++ b/src/include/catalog/pg_proc_fn.h
@@ -27,8 +27,7 @@ extern ObjectAddress ProcedureCreate(const char *procedureName,
 				Oid languageValidator,
 				const char *prosrc,
 				const char *probin,
-				bool isAgg,
-				bool isWindowFunc,
+				char prokind,
 				bool security_definer,
 				bool isLeakProof,
 				bool isStrict,
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index 4e06413cd4..64dc0187eb 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -188,7 +188,7 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 		proc->fn_tid = procTup->t_self;
 		proc->fn_readonly = (procStruct->provolatile != PROVOLATILE_VOLATILE);
 		proc->is_setof = procStruct->proretset;
-		proc->is_procedure = (procStruct->prorettype == InvalidOid);
+		proc->is_procedure = (procStruct->prokind == PROKIND_PROCEDURE);
 		proc->src = NULL;
 		proc->argnames = NULL;
 		proc->args = NULL;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index 5df4dfdf55..7e09fbf4e2 100644
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -1523,7 +1523,7 @@ compile_pltcl_function(Oid fn_oid, Oid tgreloid,
 		 * Get the required information for input conversion of the
 		 * return value.
 		 ************************************************************/
-		prodesc->fn_is_procedure = (procStruct->prorettype == InvalidOid);
+		prodesc->fn_is_procedure = (procStruct->prokind == PROKIND_PROCEDURE);
 
 		if (!is_trigger && !is_event_trigger && procStruct->prorettype)
 		{
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 44356dea0b..db9d761adf 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -83,7 +83,7 @@ ERROR:  must be owner of function alt_agg3
 ALTER AGGREGATE alt_agg2(int) SET SCHEMA alt_nsp2;  -- failed (name conflict)
 ERROR:  function alt_agg2(integer) already exists in schema "alt_nsp2"
 RESET SESSION AUTHORIZATION;
-SELECT n.nspname, proname, prorettype::regtype, proisagg, a.rolname
+SELECT n.nspname, proname, prorettype::regtype, prokind = 'a' AS proisagg, a.rolname
   FROM pg_proc p, pg_namespace n, pg_authid a
   WHERE p.pronamespace = n.oid AND p.proowner = a.oid
     AND n.nspname IN ('alt_nsp1', 'alt_nsp2')
diff --git a/src/test/regress/expected/create_function_3.out b/src/test/regress/expected/create_function_3.out
index 5ff1e0dd86..0bac62d858 100644
--- a/src/test/regress/expected/create_function_3.out
+++ b/src/test/regress/expected/create_function_3.out
@@ -271,6 +271,15 @@ ERROR:  could not find a function named "functest_b_1"
 DROP FUNCTION functest_b_2;  -- error, ambiguous
 ERROR:  function name "functest_b_2" is not unique
 HINT:  Specify the argument list to select the function unambiguously.
+-- CREATE OR REPLACE tests
+CREATE FUNCTION functest1(a int) RETURNS int LANGUAGE SQL AS 'SELECT $1';
+CREATE OR REPLACE FUNCTION functest1(a int) RETURNS int LANGUAGE SQL WINDOW AS 'SELECT $1';
+ERROR:  cannot change routine type
+DETAIL:  functest1" is a function.
+CREATE OR REPLACE PROCEDURE functest1(a int) LANGUAGE SQL AS 'SELECT $1';
+ERROR:  cannot change routine type
+DETAIL:  functest1" is a function.
+DROP FUNCTION functest1(a int);
 -- Cleanups
 DROP SCHEMA temp_func_test CASCADE;
 NOTICE:  drop cascades to 16 other objects
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 6616cc1bf0..5336c44808 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -88,10 +88,10 @@ WHERE prosrc IS NULL OR prosrc = '' OR prosrc = '-';
 -----+---------
 (0 rows)
 
--- proiswindow shouldn't be set together with proisagg or proretset
+-- proretset should only be set for normal functions
 SELECT p1.oid, p1.proname
 FROM pg_proc AS p1
-WHERE proiswindow AND (proisagg OR proretset);
+WHERE proretset AND prokind != 'f';
  oid | proname 
 -----+---------
 (0 rows)
@@ -154,9 +154,9 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid < p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    (p1.proisagg = false OR p2.proisagg = false) AND
+    (p1.prokind != 'a' OR p2.prokind != 'a') AND
     (p1.prolang != p2.prolang OR
-     p1.proisagg != p2.proisagg OR
+     p1.prokind != p2.prokind OR
      p1.prosecdef != p2.prosecdef OR
      p1.proleakproof != p2.proleakproof OR
      p1.proisstrict != p2.proisstrict OR
@@ -182,7 +182,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.prorettype < p2.prorettype)
@@ -198,7 +198,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.proargtypes[0] < p2.proargtypes[0])
@@ -216,7 +216,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.proargtypes[1] < p2.proargtypes[1])
@@ -233,7 +233,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[2] < p2.proargtypes[2])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -246,7 +246,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[3] < p2.proargtypes[3])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -259,7 +259,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[4] < p2.proargtypes[4])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -271,7 +271,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[5] < p2.proargtypes[5])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -283,7 +283,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[6] < p2.proargtypes[6])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -295,7 +295,7 @@ FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[7] < p2.proargtypes[7])
 ORDER BY 1, 2;
  proargtypes | proargtypes 
@@ -1292,15 +1292,15 @@ WHERE aggfnoid = 0 OR aggtransfn = 0 OR
 SELECT a.aggfnoid::oid, p.proname
 FROM pg_aggregate as a, pg_proc as p
 WHERE a.aggfnoid = p.oid AND
-    (NOT p.proisagg OR p.proretset OR p.pronargs < a.aggnumdirectargs);
+    (p.prokind != 'a' OR p.proretset OR p.pronargs < a.aggnumdirectargs);
  aggfnoid | proname 
 ----------+---------
 (0 rows)
 
--- Make sure there are no proisagg pg_proc entries without matches.
+-- Make sure there are no prokind = PROKIND_AGGREGATE pg_proc entries without matches.
 SELECT oid, proname
 FROM pg_proc as p
-WHERE p.proisagg AND
+WHERE p.prokind = 'a' AND
     NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid);
  oid | proname 
 -----+---------
@@ -1639,7 +1639,7 @@ ORDER BY 1, 2;
 SELECT p1.oid::regprocedure, p2.oid::regprocedure
 FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid < p2.oid AND p1.proname = p2.proname AND
-    p1.proisagg AND p2.proisagg AND
+    p1.prokind = 'a' AND p2.prokind = 'a' AND
     array_dims(p1.proargtypes) != array_dims(p2.proargtypes)
 ORDER BY 1;
      oid      |   oid   
@@ -1650,7 +1650,7 @@ ORDER BY 1;
 -- For the same reason, built-in aggregates with default arguments are no good.
 SELECT oid, proname
 FROM pg_proc AS p
-WHERE proisagg AND proargdefaults IS NOT NULL;
+WHERE prokind = 'a' AND proargdefaults IS NOT NULL;
  oid | proname 
 -----+---------
 (0 rows)
@@ -1660,7 +1660,7 @@ WHERE proisagg AND proargdefaults IS NOT NULL;
 -- that is not subject to the misplaced ORDER BY issue).
 SELECT p.oid, proname
 FROM pg_proc AS p JOIN pg_aggregate AS a ON a.aggfnoid = p.oid
-WHERE proisagg AND provariadic != 0 AND a.aggkind = 'n';
+WHERE prokind = 'a' AND provariadic != 0 AND a.aggkind = 'n';
  oid | proname 
 -----+---------
 (0 rows)
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 5433944c6a..a6e4ca5ce2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1521,9 +1521,11 @@ UNION ALL
  SELECT l.objoid,
     l.classoid,
     l.objsubid,
-        CASE
-            WHEN (pro.proisagg = true) THEN 'aggregate'::text
-            WHEN (pro.proisagg = false) THEN 'function'::text
+        CASE pro.prokind
+            WHEN 'a'::"char" THEN 'aggregate'::text
+            WHEN 'f'::"char" THEN 'function'::text
+            WHEN 'p'::"char" THEN 'procedure'::text
+            WHEN 'w'::"char" THEN 'window'::text
             ELSE NULL::text
         END AS objtype,
     pro.pronamespace AS objnamespace,
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index 96be6e752a..b3d76d01fe 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -81,7 +81,7 @@ CREATE AGGREGATE alt_agg2 (
 
 RESET SESSION AUTHORIZATION;
 
-SELECT n.nspname, proname, prorettype::regtype, proisagg, a.rolname
+SELECT n.nspname, proname, prorettype::regtype, prokind = 'a' AS proisagg, a.rolname
   FROM pg_proc p, pg_namespace n, pg_authid a
   WHERE p.pronamespace = n.oid AND p.proowner = a.oid
     AND n.nspname IN ('alt_nsp1', 'alt_nsp2')
diff --git a/src/test/regress/sql/create_function_3.sql b/src/test/regress/sql/create_function_3.sql
index fbdf8310e3..8f209d55af 100644
--- a/src/test/regress/sql/create_function_3.sql
+++ b/src/test/regress/sql/create_function_3.sql
@@ -175,6 +175,14 @@ CREATE FUNCTION functest_B_2(bigint) RETURNS bool LANGUAGE 'sql'
 DROP FUNCTION functest_b_2;  -- error, ambiguous
 
 
+-- CREATE OR REPLACE tests
+
+CREATE FUNCTION functest1(a int) RETURNS int LANGUAGE SQL AS 'SELECT $1';
+CREATE OR REPLACE FUNCTION functest1(a int) RETURNS int LANGUAGE SQL WINDOW AS 'SELECT $1';
+CREATE OR REPLACE PROCEDURE functest1(a int) LANGUAGE SQL AS 'SELECT $1';
+DROP FUNCTION functest1(a int);
+
+
 -- Cleanups
 DROP SCHEMA temp_func_test CASCADE;
 DROP USER regress_unpriv_user;
diff --git a/src/test/regress/sql/opr_sanity.sql b/src/test/regress/sql/opr_sanity.sql
index e8fdf8454d..381c14fa86 100644
--- a/src/test/regress/sql/opr_sanity.sql
+++ b/src/test/regress/sql/opr_sanity.sql
@@ -90,10 +90,10 @@
 FROM pg_proc as p1
 WHERE prosrc IS NULL OR prosrc = '' OR prosrc = '-';
 
--- proiswindow shouldn't be set together with proisagg or proretset
+-- proretset should only be set for normal functions
 SELECT p1.oid, p1.proname
 FROM pg_proc AS p1
-WHERE proiswindow AND (proisagg OR proretset);
+WHERE proretset AND prokind != 'f';
 
 -- currently, no built-in functions should be SECURITY DEFINER;
 -- this might change in future, but there will probably never be many.
@@ -140,9 +140,9 @@
 WHERE p1.oid < p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    (p1.proisagg = false OR p2.proisagg = false) AND
+    (p1.prokind != 'a' OR p2.prokind != 'a') AND
     (p1.prolang != p2.prolang OR
-     p1.proisagg != p2.proisagg OR
+     p1.prokind != p2.prokind OR
      p1.prosecdef != p2.prosecdef OR
      p1.proleakproof != p2.proleakproof OR
      p1.proisstrict != p2.proisstrict OR
@@ -166,7 +166,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.prorettype < p2.prorettype)
@@ -177,7 +177,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.proargtypes[0] < p2.proargtypes[0])
@@ -188,7 +188,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     p1.prosrc NOT LIKE E'range\\_constructor_' AND
     p2.prosrc NOT LIKE E'range\\_constructor_' AND
     (p1.proargtypes[1] < p2.proargtypes[1])
@@ -199,7 +199,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[2] < p2.proargtypes[2])
 ORDER BY 1, 2;
 
@@ -208,7 +208,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[3] < p2.proargtypes[3])
 ORDER BY 1, 2;
 
@@ -217,7 +217,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[4] < p2.proargtypes[4])
 ORDER BY 1, 2;
 
@@ -226,7 +226,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[5] < p2.proargtypes[5])
 ORDER BY 1, 2;
 
@@ -235,7 +235,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[6] < p2.proargtypes[6])
 ORDER BY 1, 2;
 
@@ -244,7 +244,7 @@
 WHERE p1.oid != p2.oid AND
     p1.prosrc = p2.prosrc AND
     p1.prolang = 12 AND p2.prolang = 12 AND
-    NOT p1.proisagg AND NOT p2.proisagg AND
+    p1.prokind != 'a' AND p2.prokind != 'a' AND
     (p1.proargtypes[7] < p2.proargtypes[7])
 ORDER BY 1, 2;
 
@@ -804,13 +804,13 @@
 SELECT a.aggfnoid::oid, p.proname
 FROM pg_aggregate as a, pg_proc as p
 WHERE a.aggfnoid = p.oid AND
-    (NOT p.proisagg OR p.proretset OR p.pronargs < a.aggnumdirectargs);
+    (p.prokind != 'a' OR p.proretset OR p.pronargs < a.aggnumdirectargs);
 
--- Make sure there are no proisagg pg_proc entries without matches.
+-- Make sure there are no prokind = PROKIND_AGGREGATE pg_proc entries without matches.
 
 SELECT oid, proname
 FROM pg_proc as p
-WHERE p.proisagg AND
+WHERE p.prokind = 'a' AND
     NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid);
 
 -- If there is no finalfn then the output type must be the transtype.
@@ -1089,7 +1089,7 @@
 SELECT p1.oid::regprocedure, p2.oid::regprocedure
 FROM pg_proc AS p1, pg_proc AS p2
 WHERE p1.oid < p2.oid AND p1.proname = p2.proname AND
-    p1.proisagg AND p2.proisagg AND
+    p1.prokind = 'a' AND p2.prokind = 'a' AND
     array_dims(p1.proargtypes) != array_dims(p2.proargtypes)
 ORDER BY 1;
 
@@ -1097,7 +1097,7 @@
 
 SELECT oid, proname
 FROM pg_proc AS p
-WHERE proisagg AND proargdefaults IS NOT NULL;
+WHERE prokind = 'a' AND proargdefaults IS NOT NULL;
 
 -- For the same reason, we avoid creating built-in variadic aggregates, except
 -- that variadic ordered-set aggregates are OK (since they have special syntax
@@ -1105,7 +1105,7 @@
 
 SELECT p.oid, proname
 FROM pg_proc AS p JOIN pg_aggregate AS a ON a.aggfnoid = p.oid
-WHERE proisagg AND provariadic != 0 AND a.aggkind = 'n';
+WHERE prokind = 'a' AND provariadic != 0 AND a.aggkind = 'n';
 
 
 -- **************** pg_opfamily ****************

base-commit: bc1adc651b8e60680aea144d51ae8bc78ea6b2fb
-- 
2.16.2

v2-0002-pg_proc.h-updates.patch.gzapplication/x-gzip; name=v2-0002-pg_proc.h-updates.patch.gzDownload
#59Tom Lane
tgl@sss.pgh.pa.us
In reply to: Peter Eisentraut (#58)
Re: prokind column (was Re: [HACKERS] SQL procedures)

Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:

Here is this patch updated. The client changes are now complete and all
the tests pass. I have also rolled back the places where the code used
prorettype to detect procedures and replaced this by the new facility.

I took a quick look through this and noted a few small problems; the
only one worth mentioning here is that you've broken psql tab completion
for functions and aggregates when talking to a pre-v11 server.
I don't think that's acceptable; however, since tab-complete.c has no
existing provisions for adjusting its queries based on server version,
it's not really clear what to do to fix it. I'm sure that's soluble
(and I think I recall a recent thread bumping up against the same
problem for another change), but it needs a bit more sweat.

We need a plan for when/how to apply this, along with the proposed
bootstrap data conversion patch, which obviously conflicts with it
significantly. Looking through the stuff pending in next month's
commitfest, we are fortunate in that only a few of those patches
seem to need to touch pg_proc.h at all, and none have really large
deltas AFAICS (I might've missed something though). I propose
proceeding as follows:

1. Get this patch in as soon as we can resolve its remaining weak
spots. That will force rebasing of pending patches that touch
pg_proc.h, but I don't think it'll be painful, since the needed
changes are pretty small and obvious.

2. During the March commitfest, adjust the bootstrap data conversion
patch to handle this change, and review it generally.

3. At the end of the 'fest, or whenever we've dealt with all other
patches that need to touch the bootstrap source data, apply the
data conversion patch.

My thought here is that the data conversion patch is going to break
basically every pending patch that touches src/include/catalog/,
so we ought to apply it at a point where that list of patches is short
and there's lots of time for people to redo them. Hence, end of the
dev cycle is the right time.

regards, tom lane

#60John Naylor
jcnaylor@gmail.com
In reply to: Tom Lane (#59)
Re: prokind column (was Re: [HACKERS] SQL procedures)

On 2/25/18, Tom Lane <tgl@sss.pgh.pa.us> wrote:

We need a plan for when/how to apply this, along with the proposed
bootstrap data conversion patch, which obviously conflicts with it
significantly.

The bulk changes in the bootstrap data patch are scripted rather than
patched, so the prokind patch will pose little in the way of
conflicts. I can't verify this just yet since Peter's second patch
doesn't apply for me against c4ba1bee68ab. Also, as of version 7 my
patch left out default values and human-readable oids, since I wanted
to get the new generated headers reviewed and up to project standards
first. Since I'll likely have to adjust the patches for those features
anyway, there's plenty of room for me to adjust to the changes to
pg_proc.h as well.

My thought here is that the data conversion patch is going to break
basically every pending patch that touches src/include/catalog/,
so we ought to apply it at a point where that list of patches is short
and there's lots of time for people to redo them. Hence, end of the
dev cycle is the right time.

I agree.

-John Naylor

#61Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Tom Lane (#59)
Re: prokind column (was Re: [HACKERS] SQL procedures)

On 2/24/18 14:08, Tom Lane wrote:

I took a quick look through this and noted a few small problems; the
only one worth mentioning here is that you've broken psql tab completion
for functions and aggregates when talking to a pre-v11 server.
I don't think that's acceptable; however, since tab-complete.c has no
existing provisions for adjusting its queries based on server version,
it's not really clear what to do to fix it. I'm sure that's soluble
(and I think I recall a recent thread bumping up against the same
problem for another change), but it needs a bit more sweat.

The patches proposed in the thread "PATCH: psql tab completion for
SELECT" appear to add support for version-dependent tab completion, so
this could be resolved "later".

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#62Tom Lane
tgl@sss.pgh.pa.us
In reply to: Peter Eisentraut (#61)
Re: prokind column (was Re: [HACKERS] SQL procedures)

Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:

On 2/24/18 14:08, Tom Lane wrote:

I took a quick look through this and noted a few small problems; the
only one worth mentioning here is that you've broken psql tab completion
for functions and aggregates when talking to a pre-v11 server.
I don't think that's acceptable; however, since tab-complete.c has no
existing provisions for adjusting its queries based on server version,
it's not really clear what to do to fix it. I'm sure that's soluble
(and I think I recall a recent thread bumping up against the same
problem for another change), but it needs a bit more sweat.

The patches proposed in the thread "PATCH: psql tab completion for
SELECT" appear to add support for version-dependent tab completion, so
this could be resolved "later".

I'm not sure that other patch will get in; AFAICS it's incomplete and
rather controversial. But I guess we could put this issue on the
open-items list so we don't forget.

Anyway, once the release dust settles, I'll try to do a proper review
of this patch. It'd be good if we could get it in this week before
the CF starts, so that any affected patches could be rebased.

regards, tom lane

#63Michael Paquier
michael@paquier.xyz
In reply to: Tom Lane (#62)
Re: prokind column (was Re: [HACKERS] SQL procedures)

On Mon, Feb 26, 2018 at 02:03:19PM -0500, Tom Lane wrote:

I'm not sure that other patch will get in; AFAICS it's incomplete and
rather controversial. But I guess we could put this issue on the
open-items list so we don't forget.

+1. We already know that we want to do a switch to prokind anyway,
while the other patch is still pending (don't think much about it
myself, I would just recommend users to use a version of psql matching
the one of the server instead of putting an extra load of maintenance
into psql for years to come). So I would recommend to push forward with
this one and fix what we know we have to fix, then request a rebase. We
gain nothing by letting things in a semi-broken state. I'll be happy to
help those folks rebase and/or write the patch to update psql's tab
completion for prokind as need be.

Anyway, once the release dust settles, I'll try to do a proper review
of this patch. It'd be good if we could get it in this week before
the CF starts, so that any affected patches could be rebased.

Here is some input from me. I don't like either that prorettype is used
to determine if the object used is a procedure or a function. The patch
series is very sensitive to changes in pg_proc.h, still those apply
correctly when using bc1adc65 as base commit.

The original commit adding support for procedures had this diff in
clauses.c:
@@ -4401,6 +4401,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
if (funcform->prolang != SQLlanguageId ||
    funcform->prosecdef ||
    funcform->proretset ||
+   funcform->prorettype == InvalidOid ||
    funcform->prorettype == RECORDOID ||

Perhaps this should be replaced with a check on prokind?

It seems to me that the same applies to fmgr_sql_validator(). In
information_schema.sql, type_udt_catalog uses a similar comparison so
this should have a comment about why using prorettype makes more sense.
In short for all those tings, it is fine to use prorettype when directly
working on the result type, like what is done in plperl and plpython.
For all the code paths working on the object type, I think that using
prokind should be the way to go.

getProcedureTypeDescription() should use an enum instead, complaining
with elog(ERROR) if the type found is something else?

I think that get_func_isagg should be dropped and replaced by
get_func_prokind.
--
Michael

#64Catalin Iacob
iacobcatalin@gmail.com
In reply to: Michael Paquier (#63)
Re: prokind column (was Re: [HACKERS] SQL procedures)

On Tue, Feb 27, 2018 at 4:03 AM, Michael Paquier <michael@paquier.xyz> wrote:

I would just recommend users to use a version of psql matching
the one of the server instead of putting an extra load of maintenance
into psql for years to come

Breaking tab completion in new psql against old servers might be
acceptable as it's a fringe feature, but I don't think your
recommendation of matching versions is practical. Lots of people
manage multiple server versions and using the latest psql for all of
them is currently, as far as I know, a perfectly supported way of
doing that, getting new psql features and keeping compatibility. I
think it would be a pity to loose that.

#65Tom Lane
tgl@sss.pgh.pa.us
In reply to: Catalin Iacob (#64)
Re: prokind column (was Re: [HACKERS] SQL procedures)

Catalin Iacob <iacobcatalin@gmail.com> writes:

On Tue, Feb 27, 2018 at 4:03 AM, Michael Paquier <michael@paquier.xyz> wrote:

I would just recommend users to use a version of psql matching
the one of the server instead of putting an extra load of maintenance
into psql for years to come

Breaking tab completion in new psql against old servers might be
acceptable as it's a fringe feature, but I don't think your
recommendation of matching versions is practical. Lots of people
manage multiple server versions and using the latest psql for all of
them is currently, as far as I know, a perfectly supported way of
doing that, getting new psql features and keeping compatibility. I
think it would be a pity to loose that.

We support the various psql/describe.c features against old servers,
so it would be quite inconsistent for tab completion not to work
similarly. There are some gaps in that already, as per the other
thread, but we should clean those up not add more.

regards, tom lane

#66Michael Paquier
michael@paquier.xyz
In reply to: Tom Lane (#65)
Re: prokind column (was Re: [HACKERS] SQL procedures)

On Tue, Feb 27, 2018 at 11:50:31AM -0500, Tom Lane wrote:

We support the various psql/describe.c features against old servers,
so it would be quite inconsistent for tab completion not to work
similarly. There are some gaps in that already, as per the other
thread, but we should clean those up not add more.

I'll try to study this thread a bit more as I lack context. So I'll
reply there. What I am scared of is that Tomas Munro has done a heroic
effort to increase the readability of psql's tab completion during the
dev cycle of 9.6. And it would be sad to reduce the quality of this
code.
--
Michael

#67Tom Lane
tgl@sss.pgh.pa.us
In reply to: Michael Paquier (#63)
3 attachment(s)
Re: prokind column (was Re: [HACKERS] SQL procedures)

Michael Paquier <michael@paquier.xyz> writes:

Here is some input from me. ...

I have reviewed this patch and attach an updated version below.
I've rebased it up to today, fixed a few minor errors, and adopted
most of Michael's suggestions. Also, since I remain desperately
unhappy with putting zeroes into prorettype, I changed it to not
do that ;-) ... now procedures have VOIDOID as their prorettype,
and it will be substantially less painful to allow them to return
some other scalar result in future, should we wish to. I believe
I've found all the places that were relying on prorettype == 0 as
a substitute for prokind == 'p'.

I have not touched the tab-complete.c changes. It doesn't really make
sense to worry about that until we see what comes out of the other thread
discussing support for server-version-sensitive tab completion. In the
meantime let's just add an open-item entry reminding us to do something
about it before v11 freezes.

Rather than manually audit the 0002 patch, I made a brain-dead little
Perl script to convert the DATA lines automatically, and attach it
below. If pg_proc.h moves further before this gets committed, the
script could be used to generate an updated 0002 patch.

One point worth noting is that in the attached, I made some edits to
pl_gram.y to preserve the existing handling of RETURN-with-a-value
in a plpgsql procedure. However, that existing handling is pretty
stupid. If we simply drop the pl_gram.y diffs below, what happens
is that RETURN-with-a-value is complained of at compile time not
execution time, which seems far superior to me (and more like what
happens in a normal function returning VOID). I think we should
do that and adjust the regression tests accordingly, but I have not
done the latter here.

I also wonder whether we should drop the stanza at the start of
pg_get_function_result() that causes it to return NULL for a procedure.
Without that, it would now return "void" which I think is at least
as defensible, and certainly more upward-compatible with any future
extension in that area. (I did make some changes in the
information_schema that cause it to report "void" not NULL in the
same case.)

Other than those points, I think this is committable, and I think we
should get it in quickly.

regards, tom lane

Attachments:

v3-0001-add-prokind-column.patchtext/x-diff; charset=us-ascii; name=v3-0001-add-prokind-column.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 71e20f2..a0e6d70 100644
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
*************** SCRAM-SHA-256$<replaceable>&lt;iteration
*** 5062,5076 ****
    </indexterm>
  
    <para>
!    The catalog <structname>pg_proc</structname> stores information about functions (or procedures).
!    See <xref linkend="sql-createfunction"/>
!    and <xref linkend="xfunc"/> for more information.
    </para>
  
    <para>
!    The table contains data for aggregate functions as well as plain functions.
!    If <structfield>proisagg</structfield> is true, there should be a matching
!    row in <structfield>pg_aggregate</structfield>.
    </para>
  
    <table>
--- 5062,5078 ----
    </indexterm>
  
    <para>
!    The catalog <structname>pg_proc</structname> stores information about
!    functions, procedures, aggregate functions, and window functions
!    (collectively also known as routines).  See <xref
!    linkend="sql-createfunction"/>, <xref linkend="sql-createprocedure"/>, and
!    <xref linkend="xfunc"/> for more information.
    </para>
  
    <para>
!    If <structfield>prokind</structfield> indicates that the entry is for an
!    aggregate function, there should be a matching row in
!    <structfield>pg_aggregate</structfield>.
    </para>
  
    <table>
*************** SCRAM-SHA-256$<replaceable>&lt;iteration
*** 5157,5173 ****
       </row>
  
       <row>
!       <entry><structfield>proisagg</structfield></entry>
!       <entry><type>bool</type></entry>
!       <entry></entry>
!       <entry>Function is an aggregate function</entry>
!      </row>
! 
!      <row>
!       <entry><structfield>proiswindow</structfield></entry>
!       <entry><type>bool</type></entry>
        <entry></entry>
!       <entry>Function is a window function</entry>
       </row>
  
       <row>
--- 5159,5170 ----
       </row>
  
       <row>
!       <entry><structfield>prokind</structfield></entry>
!       <entry><type>char</type></entry>
        <entry></entry>
!       <entry><literal>f</literal> for a normal function, <literal>p</literal>
!       for a procedure, <literal>a</literal> for an aggregate function, or
!       <literal>w</literal> for a window function</entry>
       </row>
  
       <row>
*************** SCRAM-SHA-256$<replaceable>&lt;iteration
*** 5264,5270 ****
        <entry><structfield>prorettype</structfield></entry>
        <entry><type>oid</type></entry>
        <entry><literal><link linkend="catalog-pg-type"><structname>pg_type</structname></link>.oid</literal></entry>
!       <entry>Data type of the return value, or null for a procedure</entry>
       </row>
  
       <row>
--- 5261,5267 ----
        <entry><structfield>prorettype</structfield></entry>
        <entry><type>oid</type></entry>
        <entry><literal><link linkend="catalog-pg-type"><structname>pg_type</structname></link>.oid</literal></entry>
!       <entry>Data type of the return value</entry>
       </row>
  
       <row>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 1156627..3f2c629 100644
*** a/src/backend/catalog/aclchk.c
--- b/src/backend/catalog/aclchk.c
*************** objectsInSchemaToOids(ObjectType objtype
*** 830,850 ****
  								BTEqualStrategyNumber, F_OIDEQ,
  								ObjectIdGetDatum(namespaceId));
  
- 					/*
- 					 * When looking for functions, check for return type <>0.
- 					 * When looking for procedures, check for return type ==0.
- 					 * When looking for routines, don't check the return type.
- 					 */
  					if (objtype == OBJECT_FUNCTION)
  						ScanKeyInit(&key[keycount++],
! 									Anum_pg_proc_prorettype,
! 									BTEqualStrategyNumber, F_OIDNE,
! 									InvalidOid);
  					else if (objtype == OBJECT_PROCEDURE)
  						ScanKeyInit(&key[keycount++],
! 									Anum_pg_proc_prorettype,
! 									BTEqualStrategyNumber, F_OIDEQ,
! 									InvalidOid);
  
  					rel = heap_open(ProcedureRelationId, AccessShareLock);
  					scan = heap_beginscan_catalog(rel, keycount, key);
--- 830,846 ----
  								BTEqualStrategyNumber, F_OIDEQ,
  								ObjectIdGetDatum(namespaceId));
  
  					if (objtype == OBJECT_FUNCTION)
+ 						/* includes aggregates and window functions */
  						ScanKeyInit(&key[keycount++],
! 									Anum_pg_proc_prokind,
! 									BTEqualStrategyNumber, F_CHARNE,
! 									CharGetDatum(PROKIND_PROCEDURE));
  					else if (objtype == OBJECT_PROCEDURE)
  						ScanKeyInit(&key[keycount++],
! 									Anum_pg_proc_prokind,
! 									BTEqualStrategyNumber, F_CHAREQ,
! 									CharGetDatum(PROKIND_PROCEDURE));
  
  					rel = heap_open(ProcedureRelationId, AccessShareLock);
  					scan = heap_beginscan_catalog(rel, keycount, key);
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 686528c..9a6bf1b 100644
*** a/src/backend/catalog/information_schema.sql
--- b/src/backend/catalog/information_schema.sql
*************** CREATE VIEW routines AS
*** 1413,1419 ****
             CAST(current_database() AS sql_identifier) AS routine_catalog,
             CAST(n.nspname AS sql_identifier) AS routine_schema,
             CAST(p.proname AS sql_identifier) AS routine_name,
!            CAST(CASE WHEN p.prorettype <> 0 THEN 'FUNCTION' ELSE 'PROCEDURE' END
               AS character_data) AS routine_type,
             CAST(null AS sql_identifier) AS module_catalog,
             CAST(null AS sql_identifier) AS module_schema,
--- 1413,1419 ----
             CAST(current_database() AS sql_identifier) AS routine_catalog,
             CAST(n.nspname AS sql_identifier) AS routine_schema,
             CAST(p.proname AS sql_identifier) AS routine_name,
!            CAST(CASE p.prokind WHEN 'f' THEN 'FUNCTION' WHEN 'p' THEN 'PROCEDURE' END
               AS character_data) AS routine_type,
             CAST(null AS sql_identifier) AS module_catalog,
             CAST(null AS sql_identifier) AS module_schema,
*************** CREATE VIEW routines AS
*** 1423,1430 ****
             CAST(null AS sql_identifier) AS udt_name,
  
             CAST(
!              CASE WHEN p.prorettype = 0 THEN NULL
!                   WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY'
                    WHEN nt.nspname = 'pg_catalog' THEN format_type(t.oid, null)
                    ELSE 'USER-DEFINED' END AS character_data)
               AS data_type,
--- 1423,1429 ----
             CAST(null AS sql_identifier) AS udt_name,
  
             CAST(
!              CASE WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY'
                    WHEN nt.nspname = 'pg_catalog' THEN format_type(t.oid, null)
                    ELSE 'USER-DEFINED' END AS character_data)
               AS data_type,
*************** CREATE VIEW routines AS
*** 1442,1448 ****
             CAST(null AS cardinal_number) AS datetime_precision,
             CAST(null AS character_data) AS interval_type,
             CAST(null AS cardinal_number) AS interval_precision,
!            CAST(CASE WHEN p.prorettype <> 0 THEN current_database() END AS sql_identifier) AS type_udt_catalog,
             CAST(nt.nspname AS sql_identifier) AS type_udt_schema,
             CAST(t.typname AS sql_identifier) AS type_udt_name,
             CAST(null AS sql_identifier) AS scope_catalog,
--- 1441,1447 ----
             CAST(null AS cardinal_number) AS datetime_precision,
             CAST(null AS character_data) AS interval_type,
             CAST(null AS cardinal_number) AS interval_precision,
!            CAST(current_database() AS sql_identifier) AS type_udt_catalog,
             CAST(nt.nspname AS sql_identifier) AS type_udt_schema,
             CAST(t.typname AS sql_identifier) AS type_udt_name,
             CAST(null AS sql_identifier) AS scope_catalog,
*************** CREATE VIEW routines AS
*** 1464,1470 ****
             CAST('GENERAL' AS character_data) AS parameter_style,
             CAST(CASE WHEN p.provolatile = 'i' THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_deterministic,
             CAST('MODIFIES' AS character_data) AS sql_data_access,
!            CAST(CASE WHEN p.prorettype <> 0 THEN
               CASE WHEN p.proisstrict THEN 'YES' ELSE 'NO' END END AS yes_or_no) AS is_null_call,
             CAST(null AS character_data) AS sql_path,
             CAST('YES' AS yes_or_no) AS schema_level_routine,
--- 1463,1469 ----
             CAST('GENERAL' AS character_data) AS parameter_style,
             CAST(CASE WHEN p.provolatile = 'i' THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_deterministic,
             CAST('MODIFIES' AS character_data) AS sql_data_access,
!            CAST(CASE WHEN p.prokind <> 'p' THEN
               CASE WHEN p.proisstrict THEN 'YES' ELSE 'NO' END END AS yes_or_no) AS is_null_call,
             CAST(null AS character_data) AS sql_path,
             CAST('YES' AS yes_or_no) AS schema_level_routine,
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 80f561d..119297b 100644
*** a/src/backend/catalog/objectaddress.c
--- b/src/backend/catalog/objectaddress.c
*************** getProcedureTypeDescription(StringInfo b
*** 4047,4057 ****
  		elog(ERROR, "cache lookup failed for procedure %u", procid);
  	procForm = (Form_pg_proc) GETSTRUCT(procTup);
  
! 	if (procForm->proisagg)
  		appendStringInfoString(buffer, "aggregate");
! 	else if (procForm->prorettype == InvalidOid)
  		appendStringInfoString(buffer, "procedure");
! 	else
  		appendStringInfoString(buffer, "function");
  
  	ReleaseSysCache(procTup);
--- 4047,4057 ----
  		elog(ERROR, "cache lookup failed for procedure %u", procid);
  	procForm = (Form_pg_proc) GETSTRUCT(procTup);
  
! 	if (procForm->prokind == PROKIND_AGGREGATE)
  		appendStringInfoString(buffer, "aggregate");
! 	else if (procForm->prokind == PROKIND_PROCEDURE)
  		appendStringInfoString(buffer, "procedure");
! 	else						/* function or window function */
  		appendStringInfoString(buffer, "function");
  
  	ReleaseSysCache(procTup);
diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c
index f14ea26..50d8d81 100644
*** a/src/backend/catalog/pg_aggregate.c
--- b/src/backend/catalog/pg_aggregate.c
*************** AggregateCreate(const char *aggName,
*** 616,623 ****
  							 InvalidOid,	/* no validator */
  							 "aggregate_dummy", /* placeholder proc */
  							 NULL,	/* probin */
! 							 true,	/* isAgg */
! 							 false, /* isWindowFunc */
  							 false, /* security invoker (currently not
  									 * definable for agg) */
  							 false, /* isLeakProof */
--- 616,622 ----
  							 InvalidOid,	/* no validator */
  							 "aggregate_dummy", /* placeholder proc */
  							 NULL,	/* probin */
! 							 PROKIND_AGGREGATE,
  							 false, /* security invoker (currently not
  									 * definable for agg) */
  							 false, /* isLeakProof */
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index b59fadb..f7952b9 100644
*** a/src/backend/catalog/pg_proc.c
--- b/src/backend/catalog/pg_proc.c
*************** ProcedureCreate(const char *procedureNam
*** 74,81 ****
  				Oid languageValidator,
  				const char *prosrc,
  				const char *probin,
! 				bool isAgg,
! 				bool isWindowFunc,
  				bool security_definer,
  				bool isLeakProof,
  				bool isStrict,
--- 74,80 ----
  				Oid languageValidator,
  				const char *prosrc,
  				const char *probin,
! 				char prokind,
  				bool security_definer,
  				bool isLeakProof,
  				bool isStrict,
*************** ProcedureCreate(const char *procedureNam
*** 335,342 ****
  	values[Anum_pg_proc_prorows - 1] = Float4GetDatum(prorows);
  	values[Anum_pg_proc_provariadic - 1] = ObjectIdGetDatum(variadicType);
  	values[Anum_pg_proc_protransform - 1] = ObjectIdGetDatum(InvalidOid);
! 	values[Anum_pg_proc_proisagg - 1] = BoolGetDatum(isAgg);
! 	values[Anum_pg_proc_proiswindow - 1] = BoolGetDatum(isWindowFunc);
  	values[Anum_pg_proc_prosecdef - 1] = BoolGetDatum(security_definer);
  	values[Anum_pg_proc_proleakproof - 1] = BoolGetDatum(isLeakProof);
  	values[Anum_pg_proc_proisstrict - 1] = BoolGetDatum(isStrict);
--- 334,340 ----
  	values[Anum_pg_proc_prorows - 1] = Float4GetDatum(prorows);
  	values[Anum_pg_proc_provariadic - 1] = ObjectIdGetDatum(variadicType);
  	values[Anum_pg_proc_protransform - 1] = ObjectIdGetDatum(InvalidOid);
! 	values[Anum_pg_proc_prokind - 1] = CharGetDatum(prokind);
  	values[Anum_pg_proc_prosecdef - 1] = BoolGetDatum(security_definer);
  	values[Anum_pg_proc_proleakproof - 1] = BoolGetDatum(isLeakProof);
  	values[Anum_pg_proc_proisstrict - 1] = BoolGetDatum(isStrict);
*************** ProcedureCreate(const char *procedureNam
*** 403,408 ****
--- 401,421 ----
  			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
  						   procedureName);
  
+ 		/* Not okay to change routine kind */
+ 		if (oldproc->prokind != prokind)
+ 			ereport(ERROR,
+ 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ 					 errmsg("cannot change routine type"),
+ 					 (oldproc->prokind == PROKIND_AGGREGATE ?
+ 					  errdetail("\"%s\" is an aggregate function.", procedureName) :
+ 					  oldproc->prokind == PROKIND_FUNCTION ?
+ 					  errdetail("\"%s\" is a function.", procedureName) :
+ 					  oldproc->prokind == PROKIND_PROCEDURE ?
+ 					  errdetail("\"%s\" is a procedure.", procedureName) :
+ 					  oldproc->prokind == PROKIND_WINDOW ?
+ 					  errdetail("\"%s\" is a window function.", procedureName) :
+ 					  0)));
+ 
  		/*
  		 * Not okay to change the return type of the existing proc, since
  		 * existing rules, views, etc may depend on the return type.
*************** ProcedureCreate(const char *procedureNam
*** 535,568 ****
  			}
  		}
  
- 		/* Can't change aggregate or window-function status, either */
- 		if (oldproc->proisagg != isAgg)
- 		{
- 			if (oldproc->proisagg)
- 				ereport(ERROR,
- 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- 						 errmsg("function \"%s\" is an aggregate function",
- 								procedureName)));
- 			else
- 				ereport(ERROR,
- 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- 						 errmsg("function \"%s\" is not an aggregate function",
- 								procedureName)));
- 		}
- 		if (oldproc->proiswindow != isWindowFunc)
- 		{
- 			if (oldproc->proiswindow)
- 				ereport(ERROR,
- 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- 						 errmsg("function \"%s\" is a window function",
- 								procedureName)));
- 			else
- 				ereport(ERROR,
- 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- 						 errmsg("function \"%s\" is not a window function",
- 								procedureName)));
- 		}
- 
  		/*
  		 * Do not change existing ownership or permissions, either.  Note
  		 * dependency-update code below has to agree with this decision.
--- 548,553 ----
*************** fmgr_sql_validator(PG_FUNCTION_ARGS)
*** 857,864 ****
  
  	/* Disallow pseudotype result */
  	/* except for RECORD, VOID, or polymorphic */
! 	if (proc->prorettype &&
! 		get_typtype(proc->prorettype) == TYPTYPE_PSEUDO &&
  		proc->prorettype != RECORDOID &&
  		proc->prorettype != VOIDOID &&
  		!IsPolymorphicType(proc->prorettype))
--- 842,848 ----
  
  	/* Disallow pseudotype result */
  	/* except for RECORD, VOID, or polymorphic */
! 	if (get_typtype(proc->prorettype) == TYPTYPE_PSEUDO &&
  		proc->prorettype != RECORDOID &&
  		proc->prorettype != VOIDOID &&
  		!IsPolymorphicType(proc->prorettype))
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 5652e9e..5e6e8a6 100644
*** a/src/backend/catalog/system_views.sql
--- b/src/backend/catalog/system_views.sql
*************** WHERE
*** 332,340 ****
  UNION ALL
  SELECT
  	l.objoid, l.classoid, l.objsubid,
! 	CASE WHEN pro.proisagg = true THEN 'aggregate'::text
! 	     WHEN pro.proisagg = false THEN 'function'::text
! 	END AS objtype,
  	pro.pronamespace AS objnamespace,
  	CASE WHEN pg_function_is_visible(pro.oid)
  	     THEN quote_ident(pro.proname)
--- 332,342 ----
  UNION ALL
  SELECT
  	l.objoid, l.classoid, l.objsubid,
! 	CASE pro.prokind
!             WHEN 'a' THEN 'aggregate'::text
!             WHEN 'f' THEN 'function'::text
!             WHEN 'p' THEN 'procedure'::text
!             WHEN 'w' THEN 'window'::text END AS objtype,
  	pro.pronamespace AS objnamespace,
  	CASE WHEN pg_function_is_visible(pro.oid)
  	     THEN quote_ident(pro.proname)
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index fc4ce8d..4b38ef6 100644
*** a/src/backend/commands/dropcmds.c
--- b/src/backend/commands/dropcmds.c
*************** RemoveObjects(DropStmt *stmt)
*** 92,98 ****
  		 */
  		if (stmt->removeType == OBJECT_FUNCTION)
  		{
! 			if (get_func_isagg(address.objectId))
  				ereport(ERROR,
  						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
  						 errmsg("\"%s\" is an aggregate function",
--- 92,98 ----
  		 */
  		if (stmt->removeType == OBJECT_FUNCTION)
  		{
! 			if (get_func_prokind(address.objectId) == PROKIND_AGGREGATE)
  				ereport(ERROR,
  						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
  						 errmsg("\"%s\" is an aggregate function",
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index abdfa24..b1f87d0 100644
*** a/src/backend/commands/functioncmds.c
--- b/src/backend/commands/functioncmds.c
*************** CreateFunction(ParseState *pstate, Creat
*** 1003,1011 ****
  
  	if (stmt->is_procedure)
  	{
  		Assert(!stmt->returnType);
! 
! 		prorettype = InvalidOid;
  		returnsSet = false;
  	}
  	else if (stmt->returnType)
--- 1003,1014 ----
  
  	if (stmt->is_procedure)
  	{
+ 		/*
+ 		 * Sometime in the future, procedures might be allowed to return
+ 		 * results; for now, they all return VOID.
+ 		 */
  		Assert(!stmt->returnType);
! 		prorettype = VOIDOID;
  		returnsSet = false;
  	}
  	else if (stmt->returnType)
*************** CreateFunction(ParseState *pstate, Creat
*** 1097,1104 ****
  						   languageValidator,
  						   prosrc_str,	/* converted to text later */
  						   probin_str,	/* converted to text later */
! 						   false,	/* not an aggregate */
! 						   isWindowFunc,
  						   security,
  						   isLeakProof,
  						   isStrict,
--- 1100,1106 ----
  						   languageValidator,
  						   prosrc_str,	/* converted to text later */
  						   probin_str,	/* converted to text later */
! 						   stmt->is_procedure ? PROKIND_PROCEDURE : (isWindowFunc ? PROKIND_WINDOW : PROKIND_FUNCTION),
  						   security,
  						   isLeakProof,
  						   isStrict,
*************** RemoveFunctionById(Oid funcOid)
*** 1126,1132 ****
  {
  	Relation	relation;
  	HeapTuple	tup;
! 	bool		isagg;
  
  	/*
  	 * Delete the pg_proc tuple.
--- 1128,1134 ----
  {
  	Relation	relation;
  	HeapTuple	tup;
! 	char		prokind;
  
  	/*
  	 * Delete the pg_proc tuple.
*************** RemoveFunctionById(Oid funcOid)
*** 1137,1143 ****
  	if (!HeapTupleIsValid(tup)) /* should not happen */
  		elog(ERROR, "cache lookup failed for function %u", funcOid);
  
! 	isagg = ((Form_pg_proc) GETSTRUCT(tup))->proisagg;
  
  	CatalogTupleDelete(relation, &tup->t_self);
  
--- 1139,1145 ----
  	if (!HeapTupleIsValid(tup)) /* should not happen */
  		elog(ERROR, "cache lookup failed for function %u", funcOid);
  
! 	prokind = ((Form_pg_proc) GETSTRUCT(tup))->prokind;
  
  	CatalogTupleDelete(relation, &tup->t_self);
  
*************** RemoveFunctionById(Oid funcOid)
*** 1148,1154 ****
  	/*
  	 * If there's a pg_aggregate tuple, delete that too.
  	 */
! 	if (isagg)
  	{
  		relation = heap_open(AggregateRelationId, RowExclusiveLock);
  
--- 1150,1156 ----
  	/*
  	 * If there's a pg_aggregate tuple, delete that too.
  	 */
! 	if (prokind == PROKIND_AGGREGATE)
  	{
  		relation = heap_open(AggregateRelationId, RowExclusiveLock);
  
*************** AlterFunction(ParseState *pstate, AlterF
*** 1203,1215 ****
  		aclcheck_error(ACLCHECK_NOT_OWNER, stmt->objtype,
  					   NameListToString(stmt->func->objname));
  
! 	if (procForm->proisagg)
  		ereport(ERROR,
  				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
  				 errmsg("\"%s\" is an aggregate function",
  						NameListToString(stmt->func->objname))));
  
! 	is_procedure = (procForm->prorettype == InvalidOid);
  
  	/* Examine requested actions. */
  	foreach(l, stmt->actions)
--- 1205,1217 ----
  		aclcheck_error(ACLCHECK_NOT_OWNER, stmt->objtype,
  					   NameListToString(stmt->func->objname));
  
! 	if (procForm->prokind == PROKIND_AGGREGATE)
  		ereport(ERROR,
  				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
  				 errmsg("\"%s\" is an aggregate function",
  						NameListToString(stmt->func->objname))));
  
! 	is_procedure = (procForm->prokind == PROKIND_PROCEDURE);
  
  	/* Examine requested actions. */
  	foreach(l, stmt->actions)
*************** CreateCast(CreateCastStmt *stmt)
*** 1525,1538 ****
  					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
  					 errmsg("cast function must not be volatile")));
  #endif
! 		if (procstruct->proisagg)
! 			ereport(ERROR,
! 					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
! 					 errmsg("cast function must not be an aggregate function")));
! 		if (procstruct->proiswindow)
  			ereport(ERROR,
  					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
! 					 errmsg("cast function must not be a window function")));
  		if (procstruct->proretset)
  			ereport(ERROR,
  					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
--- 1527,1536 ----
  					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
  					 errmsg("cast function must not be volatile")));
  #endif
! 		if (procstruct->prokind != PROKIND_FUNCTION)
  			ereport(ERROR,
  					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
! 					 errmsg("cast function must be a normal function")));
  		if (procstruct->proretset)
  			ereport(ERROR,
  					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
*************** check_transform_function(Form_pg_proc pr
*** 1777,1790 ****
  		ereport(ERROR,
  				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
  				 errmsg("transform function must not be volatile")));
! 	if (procstruct->proisagg)
! 		ereport(ERROR,
! 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
! 				 errmsg("transform function must not be an aggregate function")));
! 	if (procstruct->proiswindow)
  		ereport(ERROR,
  				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
! 				 errmsg("transform function must not be a window function")));
  	if (procstruct->proretset)
  		ereport(ERROR,
  				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
--- 1775,1784 ----
  		ereport(ERROR,
  				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
  				 errmsg("transform function must not be volatile")));
! 	if (procstruct->prokind != PROKIND_FUNCTION)
  		ereport(ERROR,
  				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
! 				 errmsg("transform function must be a normal function")));
  	if (procstruct->proretset)
  		ereport(ERROR,
  				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/commands/proclang.c b/src/backend/commands/proclang.c
index 2ec9624..447bd49 100644
*** a/src/backend/commands/proclang.c
--- b/src/backend/commands/proclang.c
*************** CreateProceduralLanguage(CreatePLangStmt
*** 129,136 ****
  									  F_FMGR_C_VALIDATOR,
  									  pltemplate->tmplhandler,
  									  pltemplate->tmpllibrary,
! 									  false,	/* isAgg */
! 									  false,	/* isWindowFunc */
  									  false,	/* security_definer */
  									  false,	/* isLeakProof */
  									  false,	/* isStrict */
--- 129,135 ----
  									  F_FMGR_C_VALIDATOR,
  									  pltemplate->tmplhandler,
  									  pltemplate->tmpllibrary,
! 									  PROKIND_FUNCTION,
  									  false,	/* security_definer */
  									  false,	/* isLeakProof */
  									  false,	/* isStrict */
*************** CreateProceduralLanguage(CreatePLangStmt
*** 169,176 ****
  										  F_FMGR_C_VALIDATOR,
  										  pltemplate->tmplinline,
  										  pltemplate->tmpllibrary,
! 										  false,	/* isAgg */
! 										  false,	/* isWindowFunc */
  										  false,	/* security_definer */
  										  false,	/* isLeakProof */
  										  true, /* isStrict */
--- 168,174 ----
  										  F_FMGR_C_VALIDATOR,
  										  pltemplate->tmplinline,
  										  pltemplate->tmpllibrary,
! 										  PROKIND_FUNCTION,
  										  false,	/* security_definer */
  										  false,	/* isLeakProof */
  										  true, /* isStrict */
*************** CreateProceduralLanguage(CreatePLangStmt
*** 212,219 ****
  										  F_FMGR_C_VALIDATOR,
  										  pltemplate->tmplvalidator,
  										  pltemplate->tmpllibrary,
! 										  false,	/* isAgg */
! 										  false,	/* isWindowFunc */
  										  false,	/* security_definer */
  										  false,	/* isLeakProof */
  										  true, /* isStrict */
--- 210,216 ----
  										  F_FMGR_C_VALIDATOR,
  										  pltemplate->tmplvalidator,
  										  pltemplate->tmpllibrary,
! 										  PROKIND_FUNCTION,
  										  false,	/* security_definer */
  										  false,	/* isLeakProof */
  										  true, /* isStrict */
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 899a5c4..bf3cd3a 100644
*** a/src/backend/commands/typecmds.c
--- b/src/backend/commands/typecmds.c
*************** makeRangeConstructors(const char *name, 
*** 1672,1679 ****
  								 F_FMGR_INTERNAL_VALIDATOR, /* language validator */
  								 prosrc[i], /* prosrc */
  								 NULL,	/* probin */
! 								 false, /* isAgg */
! 								 false, /* isWindowFunc */
  								 false, /* security_definer */
  								 false, /* leakproof */
  								 false, /* isStrict */
--- 1672,1678 ----
  								 F_FMGR_INTERNAL_VALIDATOR, /* language validator */
  								 prosrc[i], /* prosrc */
  								 NULL,	/* probin */
! 								 PROKIND_FUNCTION,
  								 false, /* security_definer */
  								 false, /* leakproof */
  								 false, /* isStrict */
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 7e249f5..78bc4ab 100644
*** a/src/backend/executor/functions.c
--- b/src/backend/executor/functions.c
*************** init_execution_state(List *queryTree_lis
*** 573,580 ****
  	 *
  	 * Note: don't set setsResult if the function returns VOID, as evidenced
  	 * by not having made a junkfilter.  This ensures we'll throw away any
! 	 * output from a utility statement that check_sql_fn_retval deemed to not
! 	 * have output.
  	 */
  	if (lasttages && fcache->junkFilter)
  	{
--- 573,579 ----
  	 *
  	 * Note: don't set setsResult if the function returns VOID, as evidenced
  	 * by not having made a junkfilter.  This ensures we'll throw away any
! 	 * output from the last statement in such a function.
  	 */
  	if (lasttages && fcache->junkFilter)
  	{
*************** init_sql_fcache(FmgrInfo *finfo, Oid col
*** 659,666 ****
  	fcache->rettype = rettype;
  
  	/* Fetch the typlen and byval info for the result type */
! 	if (rettype)
! 		get_typlenbyval(rettype, &fcache->typlen, &fcache->typbyval);
  
  	/* Remember whether we're returning setof something */
  	fcache->returnsSet = procedureStruct->proretset;
--- 658,664 ----
  	fcache->rettype = rettype;
  
  	/* Fetch the typlen and byval info for the result type */
! 	get_typlenbyval(rettype, &fcache->typlen, &fcache->typbyval);
  
  	/* Remember whether we're returning setof something */
  	fcache->returnsSet = procedureStruct->proretset;
*************** fmgr_sql(PG_FUNCTION_ARGS)
*** 1324,1331 ****
  		}
  		else
  		{
! 			/* Should only get here for procedures and VOID functions */
! 			Assert(fcache->rettype == InvalidOid || fcache->rettype == VOIDOID);
  			fcinfo->isnull = true;
  			result = (Datum) 0;
  		}
--- 1322,1329 ----
  		}
  		else
  		{
! 			/* Should only get here for VOID functions and procedures */
! 			Assert(fcache->rettype == VOIDOID);
  			fcinfo->isnull = true;
  			result = (Datum) 0;
  		}
*************** check_sql_fn_retval(Oid func_id, Oid ret
*** 1549,1557 ****
  	if (modifyTargetList)
  		*modifyTargetList = false;	/* initialize for no change */
  	if (junkFilter)
! 		*junkFilter = NULL;		/* initialize in case of procedure/VOID result */
  
! 	if (!rettype)
  		return false;
  
  	/*
--- 1547,1559 ----
  	if (modifyTargetList)
  		*modifyTargetList = false;	/* initialize for no change */
  	if (junkFilter)
! 		*junkFilter = NULL;		/* initialize in case of VOID result */
  
! 	/*
! 	 * If it's declared to return VOID, we don't care what's in the function.
! 	 * (This takes care of the procedure case, as well.)
! 	 */
! 	if (rettype == VOIDOID)
  		return false;
  
  	/*
*************** check_sql_fn_retval(Oid func_id, Oid ret
*** 1597,1617 ****
  	else
  	{
  		/* Empty function body, or last statement is a utility command */
! 		if (rettype && rettype != VOIDOID)
! 			ereport(ERROR,
! 					(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
! 					 errmsg("return type mismatch in function declared to return %s",
! 							format_type_be(rettype)),
! 					 errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
! 		return false;
  	}
  
  	/*
  	 * OK, check that the targetlist returns something matching the declared
! 	 * type.  (We used to insist that the declared type not be VOID in this
! 	 * case, but that makes it hard to write a void function that exits after
! 	 * calling another void function.  Instead, we insist that the tlist
! 	 * return void ... so void is treated as if it were a scalar type below.)
  	 */
  
  	/*
--- 1599,1615 ----
  	else
  	{
  		/* Empty function body, or last statement is a utility command */
! 		ereport(ERROR,
! 				(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
! 				 errmsg("return type mismatch in function declared to return %s",
! 						format_type_be(rettype)),
! 				 errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
! 		return false;			/* keep compiler quiet */
  	}
  
  	/*
  	 * OK, check that the targetlist returns something matching the declared
! 	 * type.
  	 */
  
  	/*
*************** check_sql_fn_retval(Oid func_id, Oid ret
*** 1624,1631 ****
  	if (fn_typtype == TYPTYPE_BASE ||
  		fn_typtype == TYPTYPE_DOMAIN ||
  		fn_typtype == TYPTYPE_ENUM ||
! 		fn_typtype == TYPTYPE_RANGE ||
! 		rettype == VOIDOID)
  	{
  		/*
  		 * For scalar-type returns, the target list must have exactly one
--- 1622,1628 ----
  	if (fn_typtype == TYPTYPE_BASE ||
  		fn_typtype == TYPTYPE_DOMAIN ||
  		fn_typtype == TYPTYPE_ENUM ||
! 		fn_typtype == TYPTYPE_RANGE)
  	{
  		/*
  		 * For scalar-type returns, the target list must have exactly one
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 89f27ce..a9a09af 100644
*** a/src/backend/optimizer/util/clauses.c
--- b/src/backend/optimizer/util/clauses.c
*************** inline_function(Oid funcid, Oid result_t
*** 4484,4495 ****
  
  	/*
  	 * Forget it if the function is not SQL-language or has other showstopper
! 	 * properties.  (The nargs check is just paranoia.)
  	 */
  	if (funcform->prolang != SQLlanguageId ||
  		funcform->prosecdef ||
  		funcform->proretset ||
- 		funcform->prorettype == InvalidOid ||
  		funcform->prorettype == RECORDOID ||
  		!heap_attisnull(func_tuple, Anum_pg_proc_proconfig) ||
  		funcform->pronargs != list_length(args))
--- 4484,4495 ----
  
  	/*
  	 * Forget it if the function is not SQL-language or has other showstopper
! 	 * properties.  (The prokind and nargs checks are just paranoia.)
  	 */
  	if (funcform->prolang != SQLlanguageId ||
  		funcform->prosecdef ||
+ 		funcform->prokind != PROKIND_FUNCTION ||
  		funcform->proretset ||
  		funcform->prorettype == RECORDOID ||
  		!heap_attisnull(func_tuple, Anum_pg_proc_proconfig) ||
  		funcform->pronargs != list_length(args))
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 085aa87..665d332 100644
*** a/src/backend/parser/parse_coerce.c
--- b/src/backend/parser/parse_coerce.c
*************** build_coercion_expression(Node *node,
*** 834,841 ****
  		 */
  		/* Assert(targetTypeId == procstruct->prorettype); */
  		Assert(!procstruct->proretset);
! 		Assert(!procstruct->proisagg);
! 		Assert(!procstruct->proiswindow);
  		nargs = procstruct->pronargs;
  		Assert(nargs >= 1 && nargs <= 3);
  		/* Assert(procstruct->proargtypes.values[0] == exprType(node)); */
--- 834,840 ----
  		 */
  		/* Assert(targetTypeId == procstruct->prorettype); */
  		Assert(!procstruct->proretset);
! 		Assert(procstruct->prokind == PROKIND_FUNCTION);
  		nargs = procstruct->pronargs;
  		Assert(nargs >= 1 && nargs <= 3);
  		/* Assert(procstruct->proargtypes.values[0] == exprType(node)); */
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 2a4ac09..ea5d521 100644
*** a/src/backend/parser/parse_func.c
--- b/src/backend/parser/parse_func.c
*************** func_get_detail(List *funcname,
*** 1614,1627 ****
  				*argdefaults = defaults;
  			}
  		}
! 		if (pform->proisagg)
! 			result = FUNCDETAIL_AGGREGATE;
! 		else if (pform->proiswindow)
! 			result = FUNCDETAIL_WINDOWFUNC;
! 		else if (pform->prorettype == InvalidOid)
! 			result = FUNCDETAIL_PROCEDURE;
! 		else
! 			result = FUNCDETAIL_NORMAL;
  		ReleaseSysCache(ftup);
  		return result;
  	}
--- 1614,1640 ----
  				*argdefaults = defaults;
  			}
  		}
! 
! 		switch (pform->prokind)
! 		{
! 			case PROKIND_AGGREGATE:
! 				result = FUNCDETAIL_AGGREGATE;
! 				break;
! 			case PROKIND_FUNCTION:
! 				result = FUNCDETAIL_NORMAL;
! 				break;
! 			case PROKIND_PROCEDURE:
! 				result = FUNCDETAIL_PROCEDURE;
! 				break;
! 			case PROKIND_WINDOW:
! 				result = FUNCDETAIL_WINDOWFUNC;
! 				break;
! 			default:
! 				elog(ERROR, "unrecognized prokind: %c", pform->prokind);
! 				result = FUNCDETAIL_NORMAL; /* keep compiler quiet */
! 				break;
! 		}
! 
  		ReleaseSysCache(ftup);
  		return result;
  	}
*************** LookupFuncWithArgs(ObjectType objtype, O
*** 2067,2073 ****
  	if (objtype == OBJECT_FUNCTION)
  	{
  		/* Make sure it's a function, not a procedure */
! 		if (oid && get_func_rettype(oid) == InvalidOid)
  		{
  			if (noError)
  				return InvalidOid;
--- 2080,2086 ----
  	if (objtype == OBJECT_FUNCTION)
  	{
  		/* Make sure it's a function, not a procedure */
! 		if (oid && get_func_prokind(oid) == PROKIND_PROCEDURE)
  		{
  			if (noError)
  				return InvalidOid;
*************** LookupFuncWithArgs(ObjectType objtype, O
*** 2098,2104 ****
  		}
  
  		/* Make sure it's a procedure */
! 		if (get_func_rettype(oid) != InvalidOid)
  		{
  			if (noError)
  				return InvalidOid;
--- 2111,2117 ----
  		}
  
  		/* Make sure it's a procedure */
! 		if (get_func_prokind(oid) != PROKIND_PROCEDURE)
  		{
  			if (noError)
  				return InvalidOid;
*************** LookupFuncWithArgs(ObjectType objtype, O
*** 2134,2140 ****
  		}
  
  		/* Make sure it's an aggregate */
! 		if (!get_func_isagg(oid))
  		{
  			if (noError)
  				return InvalidOid;
--- 2147,2153 ----
  		}
  
  		/* Make sure it's an aggregate */
! 		if (get_func_prokind(oid) != PROKIND_AGGREGATE)
  		{
  			if (noError)
  				return InvalidOid;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 3697466..b58ee3c 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** pg_get_functiondef(PG_FUNCTION_ARGS)
*** 2481,2492 ****
  	proc = (Form_pg_proc) GETSTRUCT(proctup);
  	name = NameStr(proc->proname);
  
! 	if (proc->proisagg)
  		ereport(ERROR,
  				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
  				 errmsg("\"%s\" is an aggregate function", name)));
  
! 	isfunction = (proc->prorettype != InvalidOid);
  
  	/*
  	 * We always qualify the function name, to ensure the right function gets
--- 2481,2492 ----
  	proc = (Form_pg_proc) GETSTRUCT(proctup);
  	name = NameStr(proc->proname);
  
! 	if (proc->prokind == PROKIND_AGGREGATE)
  		ereport(ERROR,
  				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
  				 errmsg("\"%s\" is an aggregate function", name)));
  
! 	isfunction = (proc->prokind != PROKIND_PROCEDURE);
  
  	/*
  	 * We always qualify the function name, to ensure the right function gets
*************** pg_get_functiondef(PG_FUNCTION_ARGS)
*** 2513,2519 ****
  	/* Emit some miscellaneous options on one line */
  	oldlen = buf.len;
  
! 	if (proc->proiswindow)
  		appendStringInfoString(&buf, " WINDOW");
  	switch (proc->provolatile)
  	{
--- 2513,2519 ----
  	/* Emit some miscellaneous options on one line */
  	oldlen = buf.len;
  
! 	if (proc->prokind == PROKIND_WINDOW)
  		appendStringInfoString(&buf, " WINDOW");
  	switch (proc->provolatile)
  	{
*************** pg_get_function_result(PG_FUNCTION_ARGS)
*** 2717,2723 ****
  	if (!HeapTupleIsValid(proctup))
  		PG_RETURN_NULL();
  
! 	if (((Form_pg_proc) GETSTRUCT(proctup))->prorettype == InvalidOid)
  	{
  		ReleaseSysCache(proctup);
  		PG_RETURN_NULL();
--- 2717,2723 ----
  	if (!HeapTupleIsValid(proctup))
  		PG_RETURN_NULL();
  
! 	if (((Form_pg_proc) GETSTRUCT(proctup))->prokind == PROKIND_PROCEDURE)
  	{
  		ReleaseSysCache(proctup);
  		PG_RETURN_NULL();
*************** print_function_arguments(StringInfo buf,
*** 2817,2823 ****
  	}
  
  	/* Check for special treatment of ordered-set aggregates */
! 	if (proc->proisagg)
  	{
  		HeapTuple	aggtup;
  		Form_pg_aggregate agg;
--- 2817,2823 ----
  	}
  
  	/* Check for special treatment of ordered-set aggregates */
! 	if (proc->prokind == PROKIND_AGGREGATE)
  	{
  		HeapTuple	aggtup;
  		Form_pg_aggregate agg;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 51b6b4f..bba595a 100644
*** a/src/backend/utils/cache/lsyscache.c
--- b/src/backend/utils/cache/lsyscache.c
*************** func_parallel(Oid funcid)
*** 1600,1619 ****
  }
  
  /*
!  * get_func_isagg
!  *	   Given procedure id, return the function's proisagg field.
   */
! bool
! get_func_isagg(Oid funcid)
  {
  	HeapTuple	tp;
! 	bool		result;
  
  	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
  	if (!HeapTupleIsValid(tp))
  		elog(ERROR, "cache lookup failed for function %u", funcid);
  
! 	result = ((Form_pg_proc) GETSTRUCT(tp))->proisagg;
  	ReleaseSysCache(tp);
  	return result;
  }
--- 1600,1619 ----
  }
  
  /*
!  * get_func_prokind
!  *	   Given procedure id, return the routine kind.
   */
! char
! get_func_prokind(Oid funcid)
  {
  	HeapTuple	tp;
! 	char		result;
  
  	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
  	if (!HeapTupleIsValid(tp))
  		elog(ERROR, "cache lookup failed for function %u", funcid);
  
! 	result = ((Form_pg_proc) GETSTRUCT(tp))->prokind;
  	ReleaseSysCache(tp);
  	return result;
  }
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2c934e6..566cbf2 100644
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
*************** getAggregates(Archive *fout, int *numAgg
*** 5407,5417 ****
--- 5407,5421 ----
  		PQExpBuffer racl_subquery = createPQExpBuffer();
  		PQExpBuffer initacl_subquery = createPQExpBuffer();
  		PQExpBuffer initracl_subquery = createPQExpBuffer();
+ 		const char *agg_check;
  
  		buildACLQueries(acl_subquery, racl_subquery, initacl_subquery,
  						initracl_subquery, "p.proacl", "p.proowner", "'f'",
  						dopt->binary_upgrade);
  
+ 		agg_check = (fout->remoteVersion >= 110000 ? "p.prokind = 'a'"
+ 					 : "p.proisagg");
+ 
  		appendPQExpBuffer(query, "SELECT p.tableoid, p.oid, "
  						  "p.proname AS aggname, "
  						  "p.pronamespace AS aggnamespace, "
*************** getAggregates(Archive *fout, int *numAgg
*** 5426,5432 ****
  						  "(p.oid = pip.objoid "
  						  "AND pip.classoid = 'pg_proc'::regclass "
  						  "AND pip.objsubid = 0) "
! 						  "WHERE p.proisagg AND ("
  						  "p.pronamespace != "
  						  "(SELECT oid FROM pg_namespace "
  						  "WHERE nspname = 'pg_catalog') OR "
--- 5430,5436 ----
  						  "(p.oid = pip.objoid "
  						  "AND pip.classoid = 'pg_proc'::regclass "
  						  "AND pip.objsubid = 0) "
! 						  "WHERE %s AND ("
  						  "p.pronamespace != "
  						  "(SELECT oid FROM pg_namespace "
  						  "WHERE nspname = 'pg_catalog') OR "
*************** getAggregates(Archive *fout, int *numAgg
*** 5435,5441 ****
  						  acl_subquery->data,
  						  racl_subquery->data,
  						  initacl_subquery->data,
! 						  initracl_subquery->data);
  		if (dopt->binary_upgrade)
  			appendPQExpBufferStr(query,
  								 " OR EXISTS(SELECT 1 FROM pg_depend WHERE "
--- 5439,5446 ----
  						  acl_subquery->data,
  						  racl_subquery->data,
  						  initacl_subquery->data,
! 						  initracl_subquery->data,
! 						  agg_check);
  		if (dopt->binary_upgrade)
  			appendPQExpBufferStr(query,
  								 " OR EXISTS(SELECT 1 FROM pg_depend WHERE "
*************** getFuncs(Archive *fout, int *numFuncs)
*** 5616,5626 ****
--- 5621,5635 ----
  		PQExpBuffer racl_subquery = createPQExpBuffer();
  		PQExpBuffer initacl_subquery = createPQExpBuffer();
  		PQExpBuffer initracl_subquery = createPQExpBuffer();
+ 		const char *not_agg_check;
  
  		buildACLQueries(acl_subquery, racl_subquery, initacl_subquery,
  						initracl_subquery, "p.proacl", "p.proowner", "'f'",
  						dopt->binary_upgrade);
  
+ 		not_agg_check = (fout->remoteVersion >= 110000 ? "p.prokind <> 'a'"
+ 						 : "NOT p.proisagg");
+ 
  		appendPQExpBuffer(query,
  						  "SELECT p.tableoid, p.oid, p.proname, p.prolang, "
  						  "p.pronargs, p.proargtypes, p.prorettype, "
*************** getFuncs(Archive *fout, int *numFuncs)
*** 5635,5641 ****
  						  "(p.oid = pip.objoid "
  						  "AND pip.classoid = 'pg_proc'::regclass "
  						  "AND pip.objsubid = 0) "
! 						  "WHERE NOT proisagg"
  						  "\n  AND NOT EXISTS (SELECT 1 FROM pg_depend "
  						  "WHERE classid = 'pg_proc'::regclass AND "
  						  "objid = p.oid AND deptype = 'i')"
--- 5644,5650 ----
  						  "(p.oid = pip.objoid "
  						  "AND pip.classoid = 'pg_proc'::regclass "
  						  "AND pip.objsubid = 0) "
! 						  "WHERE %s"
  						  "\n  AND NOT EXISTS (SELECT 1 FROM pg_depend "
  						  "WHERE classid = 'pg_proc'::regclass AND "
  						  "objid = p.oid AND deptype = 'i')"
*************** getFuncs(Archive *fout, int *numFuncs)
*** 5655,5660 ****
--- 5664,5670 ----
  						  initacl_subquery->data,
  						  initracl_subquery->data,
  						  username_subquery,
+ 						  not_agg_check,
  						  g_last_builtin_oid,
  						  g_last_builtin_oid);
  		if (dopt->binary_upgrade)
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11424,11435 ****
  	char	   *funcargs;
  	char	   *funciargs;
  	char	   *funcresult;
- 	bool		is_procedure;
  	char	   *proallargtypes;
  	char	   *proargmodes;
  	char	   *proargnames;
  	char	   *protrftypes;
! 	char	   *proiswindow;
  	char	   *provolatile;
  	char	   *proisstrict;
  	char	   *prosecdef;
--- 11434,11444 ----
  	char	   *funcargs;
  	char	   *funciargs;
  	char	   *funcresult;
  	char	   *proallargtypes;
  	char	   *proargmodes;
  	char	   *proargnames;
  	char	   *protrftypes;
! 	char	   *prokind;
  	char	   *provolatile;
  	char	   *proisstrict;
  	char	   *prosecdef;
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11459,11465 ****
  	asPart = createPQExpBuffer();
  
  	/* Fetch function-specific details */
! 	if (fout->remoteVersion >= 90600)
  	{
  		/*
  		 * proparallel was added in 9.6
--- 11468,11493 ----
  	asPart = createPQExpBuffer();
  
  	/* Fetch function-specific details */
! 	if (fout->remoteVersion >= 110000)
! 	{
! 		/*
! 		 * prokind was added in 11
! 		 */
! 		appendPQExpBuffer(query,
! 						  "SELECT proretset, prosrc, probin, "
! 						  "pg_catalog.pg_get_function_arguments(oid) AS funcargs, "
! 						  "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
! 						  "pg_catalog.pg_get_function_result(oid) AS funcresult, "
! 						  "array_to_string(protrftypes, ' ') AS protrftypes, "
! 						  "prokind, provolatile, proisstrict, prosecdef, "
! 						  "proleakproof, proconfig, procost, prorows, "
! 						  "proparallel, "
! 						  "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
! 						  "FROM pg_catalog.pg_proc "
! 						  "WHERE oid = '%u'::pg_catalog.oid",
! 						  finfo->dobj.catId.oid);
! 	}
! 	else if (fout->remoteVersion >= 90600)
  	{
  		/*
  		 * proparallel was added in 9.6
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11470,11476 ****
  						  "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
  						  "pg_catalog.pg_get_function_result(oid) AS funcresult, "
  						  "array_to_string(protrftypes, ' ') AS protrftypes, "
! 						  "proiswindow, provolatile, proisstrict, prosecdef, "
  						  "proleakproof, proconfig, procost, prorows, "
  						  "proparallel, "
  						  "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
--- 11498,11505 ----
  						  "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
  						  "pg_catalog.pg_get_function_result(oid) AS funcresult, "
  						  "array_to_string(protrftypes, ' ') AS protrftypes, "
! 						  "CASE WHEN proiswindow THEN 'w' ELSE 'f' END AS prokind, "
! 						  "provolatile, proisstrict, prosecdef, "
  						  "proleakproof, proconfig, procost, prorows, "
  						  "proparallel, "
  						  "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11489,11495 ****
  						  "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
  						  "pg_catalog.pg_get_function_result(oid) AS funcresult, "
  						  "array_to_string(protrftypes, ' ') AS protrftypes, "
! 						  "proiswindow, provolatile, proisstrict, prosecdef, "
  						  "proleakproof, proconfig, procost, prorows, "
  						  "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
  						  "FROM pg_catalog.pg_proc "
--- 11518,11525 ----
  						  "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
  						  "pg_catalog.pg_get_function_result(oid) AS funcresult, "
  						  "array_to_string(protrftypes, ' ') AS protrftypes, "
! 						  "CASE WHEN proiswindow THEN 'w' ELSE 'f' END AS prokind, "
! 						  "provolatile, proisstrict, prosecdef, "
  						  "proleakproof, proconfig, procost, prorows, "
  						  "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
  						  "FROM pg_catalog.pg_proc "
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11506,11512 ****
  						  "pg_catalog.pg_get_function_arguments(oid) AS funcargs, "
  						  "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
  						  "pg_catalog.pg_get_function_result(oid) AS funcresult, "
! 						  "proiswindow, provolatile, proisstrict, prosecdef, "
  						  "proleakproof, proconfig, procost, prorows, "
  						  "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
  						  "FROM pg_catalog.pg_proc "
--- 11536,11543 ----
  						  "pg_catalog.pg_get_function_arguments(oid) AS funcargs, "
  						  "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
  						  "pg_catalog.pg_get_function_result(oid) AS funcresult, "
! 						  "CASE WHEN proiswindow THEN 'w' ELSE 'f' END AS prokind, "
! 						  "provolatile, proisstrict, prosecdef, "
  						  "proleakproof, proconfig, procost, prorows, "
  						  "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
  						  "FROM pg_catalog.pg_proc "
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11524,11530 ****
  						  "pg_catalog.pg_get_function_arguments(oid) AS funcargs, "
  						  "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
  						  "pg_catalog.pg_get_function_result(oid) AS funcresult, "
! 						  "proiswindow, provolatile, proisstrict, prosecdef, "
  						  "false AS proleakproof, "
  						  " proconfig, procost, prorows, "
  						  "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
--- 11555,11562 ----
  						  "pg_catalog.pg_get_function_arguments(oid) AS funcargs, "
  						  "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
  						  "pg_catalog.pg_get_function_result(oid) AS funcresult, "
! 						  "CASE WHEN proiswindow THEN 'w' ELSE 'f' END AS prokind, "
! 						  "provolatile, proisstrict, prosecdef, "
  						  "false AS proleakproof, "
  						  " proconfig, procost, prorows, "
  						  "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11537,11543 ****
  		appendPQExpBuffer(query,
  						  "SELECT proretset, prosrc, probin, "
  						  "proallargtypes, proargmodes, proargnames, "
! 						  "false AS proiswindow, "
  						  "provolatile, proisstrict, prosecdef, "
  						  "false AS proleakproof, "
  						  "proconfig, procost, prorows, "
--- 11569,11575 ----
  		appendPQExpBuffer(query,
  						  "SELECT proretset, prosrc, probin, "
  						  "proallargtypes, proargmodes, proargnames, "
! 						  "'f' AS prokind, "
  						  "provolatile, proisstrict, prosecdef, "
  						  "false AS proleakproof, "
  						  "proconfig, procost, prorows, "
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11551,11557 ****
  		appendPQExpBuffer(query,
  						  "SELECT proretset, prosrc, probin, "
  						  "proallargtypes, proargmodes, proargnames, "
! 						  "false AS proiswindow, "
  						  "provolatile, proisstrict, prosecdef, "
  						  "false AS proleakproof, "
  						  "null AS proconfig, 0 AS procost, 0 AS prorows, "
--- 11583,11589 ----
  		appendPQExpBuffer(query,
  						  "SELECT proretset, prosrc, probin, "
  						  "proallargtypes, proargmodes, proargnames, "
! 						  "'f' AS prokind, "
  						  "provolatile, proisstrict, prosecdef, "
  						  "false AS proleakproof, "
  						  "null AS proconfig, 0 AS procost, 0 AS prorows, "
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11567,11573 ****
  						  "null AS proallargtypes, "
  						  "null AS proargmodes, "
  						  "proargnames, "
! 						  "false AS proiswindow, "
  						  "provolatile, proisstrict, prosecdef, "
  						  "false AS proleakproof, "
  						  "null AS proconfig, 0 AS procost, 0 AS prorows, "
--- 11599,11605 ----
  						  "null AS proallargtypes, "
  						  "null AS proargmodes, "
  						  "proargnames, "
! 						  "'f' AS prokind, "
  						  "provolatile, proisstrict, prosecdef, "
  						  "false AS proleakproof, "
  						  "null AS proconfig, 0 AS procost, 0 AS prorows, "
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11586,11596 ****
  	{
  		funcargs = PQgetvalue(res, 0, PQfnumber(res, "funcargs"));
  		funciargs = PQgetvalue(res, 0, PQfnumber(res, "funciargs"));
! 		is_procedure = PQgetisnull(res, 0, PQfnumber(res, "funcresult"));
! 		if (is_procedure)
! 			funcresult = NULL;
! 		else
! 			funcresult = PQgetvalue(res, 0, PQfnumber(res, "funcresult"));
  		proallargtypes = proargmodes = proargnames = NULL;
  	}
  	else
--- 11618,11624 ----
  	{
  		funcargs = PQgetvalue(res, 0, PQfnumber(res, "funcargs"));
  		funciargs = PQgetvalue(res, 0, PQfnumber(res, "funciargs"));
! 		funcresult = PQgetvalue(res, 0, PQfnumber(res, "funcresult"));
  		proallargtypes = proargmodes = proargnames = NULL;
  	}
  	else
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11599,11611 ****
  		proargmodes = PQgetvalue(res, 0, PQfnumber(res, "proargmodes"));
  		proargnames = PQgetvalue(res, 0, PQfnumber(res, "proargnames"));
  		funcargs = funciargs = funcresult = NULL;
- 		is_procedure = false;
  	}
  	if (PQfnumber(res, "protrftypes") != -1)
  		protrftypes = PQgetvalue(res, 0, PQfnumber(res, "protrftypes"));
  	else
  		protrftypes = NULL;
! 	proiswindow = PQgetvalue(res, 0, PQfnumber(res, "proiswindow"));
  	provolatile = PQgetvalue(res, 0, PQfnumber(res, "provolatile"));
  	proisstrict = PQgetvalue(res, 0, PQfnumber(res, "proisstrict"));
  	prosecdef = PQgetvalue(res, 0, PQfnumber(res, "prosecdef"));
--- 11627,11638 ----
  		proargmodes = PQgetvalue(res, 0, PQfnumber(res, "proargmodes"));
  		proargnames = PQgetvalue(res, 0, PQfnumber(res, "proargnames"));
  		funcargs = funciargs = funcresult = NULL;
  	}
  	if (PQfnumber(res, "protrftypes") != -1)
  		protrftypes = PQgetvalue(res, 0, PQfnumber(res, "protrftypes"));
  	else
  		protrftypes = NULL;
! 	prokind = PQgetvalue(res, 0, PQfnumber(res, "prokind"));
  	provolatile = PQgetvalue(res, 0, PQfnumber(res, "provolatile"));
  	proisstrict = PQgetvalue(res, 0, PQfnumber(res, "proisstrict"));
  	prosecdef = PQgetvalue(res, 0, PQfnumber(res, "prosecdef"));
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11731,11737 ****
  
  	funcsig_tag = format_function_signature(fout, finfo, false);
  
! 	keyword = is_procedure ? "PROCEDURE" : "FUNCTION";
  
  	appendPQExpBuffer(delqry, "DROP %s %s.%s;\n",
  					  keyword,
--- 11758,11767 ----
  
  	funcsig_tag = format_function_signature(fout, finfo, false);
  
! 	if (prokind[0] == PROKIND_PROCEDURE)
! 		keyword = "PROCEDURE";
! 	else
! 		keyword = "FUNCTION";	/* works for window functions too */
  
  	appendPQExpBuffer(delqry, "DROP %s %s.%s;\n",
  					  keyword,
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11744,11751 ****
  					  funcfullsig ? funcfullsig :
  					  funcsig);
  
! 	if (is_procedure)
! 		;
  	else if (funcresult)
  		appendPQExpBuffer(q, " RETURNS %s", funcresult);
  	else
--- 11774,11781 ----
  					  funcfullsig ? funcfullsig :
  					  funcsig);
  
! 	if (prokind[0] == PROKIND_PROCEDURE)
! 		 /* no result type to output */ ;
  	else if (funcresult)
  		appendPQExpBuffer(q, " RETURNS %s", funcresult);
  	else
*************** dumpFunc(Archive *fout, FuncInfo *finfo)
*** 11776,11782 ****
  		}
  	}
  
! 	if (proiswindow[0] == 't')
  		appendPQExpBufferStr(q, " WINDOW");
  
  	if (provolatile[0] != PROVOLATILE_VOLATILE)
--- 11806,11812 ----
  		}
  	}
  
! 	if (prokind[0] == PROKIND_WINDOW)
  		appendPQExpBufferStr(q, " WINDOW");
  
  	if (provolatile[0] != PROVOLATILE_VOLATILE)
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 6f74e15..1bea6ae 100644
*** a/src/bin/pg_dump/t/002_pg_dump.pl
--- b/src/bin/pg_dump/t/002_pg_dump.pl
*************** qr/^GRANT SELECT ON TABLE dump_test_seco
*** 6257,6264 ****
  						   prorows,
  						   provariadic,
  						   protransform,
! 						   proisagg,
! 						   proiswindow,
  						   prosecdef,
  						   proleakproof,
  						   proisstrict,
--- 6257,6263 ----
  						   prorows,
  						   provariadic,
  						   protransform,
! 						   prokind,
  						   prosecdef,
  						   proleakproof,
  						   proisstrict,
*************** qr/^GRANT SELECT ON TABLE dump_test_seco
*** 6290,6297 ****
  		\QGRANT SELECT(prorows) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
  		\QGRANT SELECT(provariadic) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
  		\QGRANT SELECT(protransform) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
! 		\QGRANT SELECT(proisagg) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
! 		\QGRANT SELECT(proiswindow) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
  		\QGRANT SELECT(prosecdef) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
  		\QGRANT SELECT(proleakproof) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
  		\QGRANT SELECT(proisstrict) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
--- 6289,6295 ----
  		\QGRANT SELECT(prorows) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
  		\QGRANT SELECT(provariadic) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
  		\QGRANT SELECT(protransform) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
! 		\QGRANT SELECT(prokind) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
  		\QGRANT SELECT(prosecdef) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
  		\QGRANT SELECT(proleakproof) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
  		\QGRANT SELECT(proisstrict) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 466a780..0c3be1f 100644
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** describeAggregates(const char *pattern, 
*** 100,111 ****
  						  "  pg_catalog.format_type(p.proargtypes[0], NULL) AS \"%s\",\n",
  						  gettext_noop("Argument data types"));
  
! 	appendPQExpBuffer(&buf,
! 					  "  pg_catalog.obj_description(p.oid, 'pg_proc') as \"%s\"\n"
! 					  "FROM pg_catalog.pg_proc p\n"
! 					  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n"
! 					  "WHERE p.proisagg\n",
! 					  gettext_noop("Description"));
  
  	if (!showSystem && !pattern)
  		appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
--- 100,119 ----
  						  "  pg_catalog.format_type(p.proargtypes[0], NULL) AS \"%s\",\n",
  						  gettext_noop("Argument data types"));
  
! 	if (pset.sversion >= 110000)
! 		appendPQExpBuffer(&buf,
! 						  "  pg_catalog.obj_description(p.oid, 'pg_proc') as \"%s\"\n"
! 						  "FROM pg_catalog.pg_proc p\n"
! 						  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n"
! 						  "WHERE p.prokind = 'a'\n",
! 						  gettext_noop("Description"));
! 	else
! 		appendPQExpBuffer(&buf,
! 						  "  pg_catalog.obj_description(p.oid, 'pg_proc') as \"%s\"\n"
! 						  "FROM pg_catalog.pg_proc p\n"
! 						  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n"
! 						  "WHERE p.proisagg\n",
! 						  gettext_noop("Description"));
  
  	if (!showSystem && !pattern)
  		appendPQExpBufferStr(&buf, "      AND n.nspname <> 'pg_catalog'\n"
*************** describeFunctions(const char *functypes,
*** 346,359 ****
  					  gettext_noop("Schema"),
  					  gettext_noop("Name"));
  
! 	if (pset.sversion >= 80400)
  		appendPQExpBuffer(&buf,
  						  "  pg_catalog.pg_get_function_result(p.oid) as \"%s\",\n"
  						  "  pg_catalog.pg_get_function_arguments(p.oid) as \"%s\",\n"
  						  " CASE\n"
  						  "  WHEN p.proisagg THEN '%s'\n"
  						  "  WHEN p.proiswindow THEN '%s'\n"
- 						  "  WHEN p.prorettype = 0 THEN '%s'\n"
  						  "  WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN '%s'\n"
  						  "  ELSE '%s'\n"
  						  " END as \"%s\"",
--- 354,384 ----
  					  gettext_noop("Schema"),
  					  gettext_noop("Name"));
  
! 	if (pset.sversion >= 110000)
! 		appendPQExpBuffer(&buf,
! 						  "  pg_catalog.pg_get_function_result(p.oid) as \"%s\",\n"
! 						  "  pg_catalog.pg_get_function_arguments(p.oid) as \"%s\",\n"
! 						  " CASE p.prokind\n"
! 						  "  WHEN 'a' THEN '%s'\n"
! 						  "  WHEN 'w' THEN '%s'\n"
! 						  "  WHEN 'p' THEN '%s'\n"
! 						  "  ELSE '%s'\n"
! 						  " END as \"%s\"",
! 						  gettext_noop("Result data type"),
! 						  gettext_noop("Argument data types"),
! 		/* translator: "agg" is short for "aggregate" */
! 						  gettext_noop("agg"),
! 						  gettext_noop("window"),
! 						  gettext_noop("proc"),
! 						  gettext_noop("func"),
! 						  gettext_noop("Type"));
! 	else if (pset.sversion >= 80400)
  		appendPQExpBuffer(&buf,
  						  "  pg_catalog.pg_get_function_result(p.oid) as \"%s\",\n"
  						  "  pg_catalog.pg_get_function_arguments(p.oid) as \"%s\",\n"
  						  " CASE\n"
  						  "  WHEN p.proisagg THEN '%s'\n"
  						  "  WHEN p.proiswindow THEN '%s'\n"
  						  "  WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN '%s'\n"
  						  "  ELSE '%s'\n"
  						  " END as \"%s\"",
*************** describeFunctions(const char *functypes,
*** 362,368 ****
  		/* translator: "agg" is short for "aggregate" */
  						  gettext_noop("agg"),
  						  gettext_noop("window"),
- 						  gettext_noop("proc"),
  						  gettext_noop("trigger"),
  						  gettext_noop("func"),
  						  gettext_noop("Type"));
--- 387,392 ----
*************** describeFunctions(const char *functypes,
*** 494,500 ****
  				appendPQExpBufferStr(&buf, "WHERE ");
  				have_where = true;
  			}
! 			appendPQExpBufferStr(&buf, "NOT p.proisagg\n");
  		}
  		if (!showTrigger)
  		{
--- 518,527 ----
  				appendPQExpBufferStr(&buf, "WHERE ");
  				have_where = true;
  			}
! 			if (pset.sversion >= 110000)
! 				appendPQExpBufferStr(&buf, "p.prokind <> 'a'\n");
! 			else
! 				appendPQExpBufferStr(&buf, "NOT p.proisagg\n");
  		}
  		if (!showTrigger)
  		{
*************** describeFunctions(const char *functypes,
*** 516,522 ****
  				appendPQExpBufferStr(&buf, "WHERE ");
  				have_where = true;
  			}
! 			appendPQExpBufferStr(&buf, "NOT p.proiswindow\n");
  		}
  	}
  	else
--- 543,552 ----
  				appendPQExpBufferStr(&buf, "WHERE ");
  				have_where = true;
  			}
! 			if (pset.sversion >= 110000)
! 				appendPQExpBufferStr(&buf, "p.prokind <> 'w'\n");
! 			else
! 				appendPQExpBufferStr(&buf, "NOT p.proiswindow\n");
  		}
  	}
  	else
*************** describeFunctions(const char *functypes,
*** 528,534 ****
  		/* Note: at least one of these must be true ... */
  		if (showAggregate)
  		{
! 			appendPQExpBufferStr(&buf, "p.proisagg\n");
  			needs_or = true;
  		}
  		if (showTrigger)
--- 558,567 ----
  		/* Note: at least one of these must be true ... */
  		if (showAggregate)
  		{
! 			if (pset.sversion >= 110000)
! 				appendPQExpBufferStr(&buf, "p.prokind = 'a'\n");
! 			else
! 				appendPQExpBufferStr(&buf, "p.proisagg\n");
  			needs_or = true;
  		}
  		if (showTrigger)
*************** describeFunctions(const char *functypes,
*** 543,549 ****
  		{
  			if (needs_or)
  				appendPQExpBufferStr(&buf, "       OR ");
! 			appendPQExpBufferStr(&buf, "p.proiswindow\n");
  			needs_or = true;
  		}
  		appendPQExpBufferStr(&buf, "      )\n");
--- 576,585 ----
  		{
  			if (needs_or)
  				appendPQExpBufferStr(&buf, "       OR ");
! 			if (pset.sversion >= 110000)
! 				appendPQExpBufferStr(&buf, "p.prokind = 'w'\n");
! 			else
! 				appendPQExpBufferStr(&buf, "p.proiswindow\n");
  			needs_or = true;
  		}
  		appendPQExpBufferStr(&buf, "      )\n");
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 8bc4a19..cf23d13 100644
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
*************** static const SchemaQuery Query_for_list_
*** 349,355 ****
  	/* catname */
  	"pg_catalog.pg_proc p",
  	/* selcondition */
! 	"p.proisagg",
  	/* viscondition */
  	"pg_catalog.pg_function_is_visible(p.oid)",
  	/* namespace */
--- 349,355 ----
  	/* catname */
  	"pg_catalog.pg_proc p",
  	/* selcondition */
! 	"p.prokind = 'a'",
  	/* viscondition */
  	"pg_catalog.pg_function_is_visible(p.oid)",
  	/* namespace */
*************** static const SchemaQuery Query_for_list_
*** 397,403 ****
  	/* catname */
  	"pg_catalog.pg_proc p",
  	/* selcondition */
! 	"p.prorettype <> 0",
  	/* viscondition */
  	"pg_catalog.pg_function_is_visible(p.oid)",
  	/* namespace */
--- 397,403 ----
  	/* catname */
  	"pg_catalog.pg_proc p",
  	/* selcondition */
! 	"p.prokind IN ('f', 'w')",
  	/* viscondition */
  	"pg_catalog.pg_function_is_visible(p.oid)",
  	/* namespace */
*************** static const SchemaQuery Query_for_list_
*** 428,434 ****
  	/* catname */
  	"pg_catalog.pg_proc p",
  	/* selcondition */
! 	"p.prorettype = 0",
  	/* viscondition */
  	"pg_catalog.pg_function_is_visible(p.oid)",
  	/* namespace */
--- 428,434 ----
  	/* catname */
  	"pg_catalog.pg_proc p",
  	/* selcondition */
! 	"p.prokind = 'p'",
  	/* viscondition */
  	"pg_catalog.pg_function_is_visible(p.oid)",
  	/* namespace */
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 26b1866..01cf59e 100644
*** a/src/include/catalog/pg_class.h
--- b/src/include/catalog/pg_class.h
*************** DATA(insert OID = 1247 (  pg_type		PGNSP
*** 151,157 ****
  DESCR("");
  DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 22 0 f f f f f f f t n f 3 1 _null_ _null_ _null_));
  DESCR("");
! DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n f 3 1 _null_ _null_ _null_));
  DESCR("");
  DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 33 0 t f f f f f f t n f 3 1 _null_ _null_ _null_));
  DESCR("");
--- 151,157 ----
  DESCR("");
  DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 22 0 f f f f f f f t n f 3 1 _null_ _null_ _null_));
  DESCR("");
! DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f f t n f 3 1 _null_ _null_ _null_));
  DESCR("");
  DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 33 0 t f f f f f f t n f 3 1 _null_ _null_ _null_));
  DESCR("");
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index c00d055..b25c918 100644
*** a/src/include/catalog/pg_proc.h
--- b/src/include/catalog/pg_proc.h
*************** CATALOG(pg_proc,1255) BKI_BOOTSTRAP BKI_
*** 43,50 ****
  	float4		prorows;		/* estimated # of rows out (if proretset) */
  	Oid			provariadic;	/* element type of variadic array, or 0 */
  	regproc		protransform;	/* transforms calls to it during planning */
! 	bool		proisagg;		/* is it an aggregate? */
! 	bool		proiswindow;	/* is it a window function? */
  	bool		prosecdef;		/* security definer */
  	bool		proleakproof;	/* is it a leak-proof function? */
  	bool		proisstrict;	/* strict with respect to NULLs? */
--- 43,49 ----
  	float4		prorows;		/* estimated # of rows out (if proretset) */
  	Oid			provariadic;	/* element type of variadic array, or 0 */
  	regproc		protransform;	/* transforms calls to it during planning */
! 	char		prokind;		/* see PROKIND_ categories below */
  	bool		prosecdef;		/* security definer */
  	bool		proleakproof;	/* is it a leak-proof function? */
  	bool		proisstrict;	/* strict with respect to NULLs? */
*************** typedef FormData_pg_proc *Form_pg_proc;
*** 86,92 ****
   *		compiler constants for pg_proc
   * ----------------
   */
! #define Natts_pg_proc					29
  #define Anum_pg_proc_proname			1
  #define Anum_pg_proc_pronamespace		2
  #define Anum_pg_proc_proowner			3
--- 85,91 ----
   *		compiler constants for pg_proc
   * ----------------
   */
! #define Natts_pg_proc					28
  #define Anum_pg_proc_proname			1
  #define Anum_pg_proc_pronamespace		2
  #define Anum_pg_proc_proowner			3
*************** typedef FormData_pg_proc *Form_pg_proc;
*** 95,121 ****
  #define Anum_pg_proc_prorows			6
  #define Anum_pg_proc_provariadic		7
  #define Anum_pg_proc_protransform		8
! #define Anum_pg_proc_proisagg			9
! #define Anum_pg_proc_proiswindow		10
! #define Anum_pg_proc_prosecdef			11
! #define Anum_pg_proc_proleakproof		12
! #define Anum_pg_proc_proisstrict		13
! #define Anum_pg_proc_proretset			14
! #define Anum_pg_proc_provolatile		15
! #define Anum_pg_proc_proparallel		16
! #define Anum_pg_proc_pronargs			17
! #define Anum_pg_proc_pronargdefaults	18
! #define Anum_pg_proc_prorettype			19
! #define Anum_pg_proc_proargtypes		20
! #define Anum_pg_proc_proallargtypes		21
! #define Anum_pg_proc_proargmodes		22
! #define Anum_pg_proc_proargnames		23
! #define Anum_pg_proc_proargdefaults		24
! #define Anum_pg_proc_protrftypes		25
! #define Anum_pg_proc_prosrc				26
! #define Anum_pg_proc_probin				27
! #define Anum_pg_proc_proconfig			28
! #define Anum_pg_proc_proacl				29
  
  /* ----------------
   *		initial contents of pg_proc
--- 94,119 ----
  #define Anum_pg_proc_prorows			6
  #define Anum_pg_proc_provariadic		7
  #define Anum_pg_proc_protransform		8
! #define Anum_pg_proc_prokind			9
! #define Anum_pg_proc_prosecdef			10
! #define Anum_pg_proc_proleakproof		11
! #define Anum_pg_proc_proisstrict		12
! #define Anum_pg_proc_proretset			13
! #define Anum_pg_proc_provolatile		14
! #define Anum_pg_proc_proparallel		15
! #define Anum_pg_proc_pronargs			16
! #define Anum_pg_proc_pronargdefaults	17
! #define Anum_pg_proc_prorettype			18
! #define Anum_pg_proc_proargtypes		19
! #define Anum_pg_proc_proallargtypes		20
! #define Anum_pg_proc_proargmodes		21
! #define Anum_pg_proc_proargnames		22
! #define Anum_pg_proc_proargdefaults		23
! #define Anum_pg_proc_protrftypes		24
! #define Anum_pg_proc_prosrc				25
! #define Anum_pg_proc_probin				26
! #define Anum_pg_proc_proconfig			27
! #define Anum_pg_proc_proacl				28
  
  /* ----------------
   *		initial contents of pg_proc
*************** DATA(insert OID = 5028 ( satisfies_hash_
*** 5576,5581 ****
--- 5574,5587 ----
  DESCR("hash partition CHECK constraint");
  
  /*
+  * Symbolic values for prokind column
+  */
+ #define PROKIND_FUNCTION 'f'
+ #define PROKIND_AGGREGATE 'a'
+ #define PROKIND_WINDOW 'w'
+ #define PROKIND_PROCEDURE 'p'
+ 
+ /*
   * Symbolic values for provolatile column: these indicate whether the result
   * of a function is dependent *only* on the values of its explicit arguments,
   * or can change due to outside factors (such as parameter variables or
diff --git a/src/include/catalog/pg_proc_fn.h b/src/include/catalog/pg_proc_fn.h
index 098e2e6..b66871b 100644
*** a/src/include/catalog/pg_proc_fn.h
--- b/src/include/catalog/pg_proc_fn.h
*************** extern ObjectAddress ProcedureCreate(con
*** 27,34 ****
  				Oid languageValidator,
  				const char *prosrc,
  				const char *probin,
! 				bool isAgg,
! 				bool isWindowFunc,
  				bool security_definer,
  				bool isLeakProof,
  				bool isStrict,
--- 27,33 ----
  				Oid languageValidator,
  				const char *prosrc,
  				const char *probin,
! 				char prokind,
  				bool security_definer,
  				bool isLeakProof,
  				bool isStrict,
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 1f6c04a..e55ea40 100644
*** a/src/include/utils/lsyscache.h
--- b/src/include/utils/lsyscache.h
*************** extern bool get_func_retset(Oid funcid);
*** 117,123 ****
  extern bool func_strict(Oid funcid);
  extern char func_volatile(Oid funcid);
  extern char func_parallel(Oid funcid);
! extern bool get_func_isagg(Oid funcid);
  extern bool get_func_leakproof(Oid funcid);
  extern float4 get_func_cost(Oid funcid);
  extern float4 get_func_rows(Oid funcid);
--- 117,123 ----
  extern bool func_strict(Oid funcid);
  extern char func_volatile(Oid funcid);
  extern char func_parallel(Oid funcid);
! extern char get_func_prokind(Oid funcid);
  extern bool get_func_leakproof(Oid funcid);
  extern float4 get_func_cost(Oid funcid);
  extern float4 get_func_rows(Oid funcid);
diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index 77c41b2..fa8e2fd 100644
*** a/src/pl/plperl/plperl.c
--- b/src/pl/plperl/plperl.c
*************** compile_plperl_function(Oid fn_oid, bool
*** 2832,2838 ****
  		 * Get the required information for input conversion of the
  		 * return value.
  		 ************************************************************/
! 		if (!is_trigger && !is_event_trigger && procStruct->prorettype)
  		{
  			Oid			rettype = procStruct->prorettype;
  
--- 2832,2839 ----
  		 * Get the required information for input conversion of the
  		 * return value.
  		 ************************************************************/
! 		if (!is_trigger && !is_event_trigger &&
! 			procStruct->prokind != PROKIND_PROCEDURE)
  		{
  			Oid			rettype = procStruct->prorettype;
  
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index d07a16a..6fbbc4a 100644
*** a/src/pl/plpgsql/src/pl_comp.c
--- b/src/pl/plpgsql/src/pl_comp.c
*************** do_compile(FunctionCallInfo fcinfo,
*** 275,280 ****
--- 275,281 ----
  	bool		isnull;
  	char	   *proc_source;
  	HeapTuple	typeTup;
+ 	Form_pg_type typeStruct;
  	PLpgSQL_variable *var;
  	PLpgSQL_rec *rec;
  	int			i;
*************** do_compile(FunctionCallInfo fcinfo,
*** 365,370 ****
--- 366,373 ----
  	else
  		function->fn_is_trigger = PLPGSQL_NOT_TRIGGER;
  
+ 	function->fn_prokind = procStruct->prokind;
+ 
  	/*
  	 * Initialize the compiler, particularly the namespace stack.  The
  	 * outermost namespace contains function parameters and other special
*************** do_compile(FunctionCallInfo fcinfo,
*** 529,538 ****
  			/*
  			 * Lookup the function's return type
  			 */
- 			if (rettypeid)
- 			{
- 				Form_pg_type typeStruct;
- 
  				typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rettypeid));
  				if (!HeapTupleIsValid(typeTup))
  					elog(ERROR, "cache lookup failed for type %u", rettypeid);
--- 532,537 ----
*************** do_compile(FunctionCallInfo fcinfo,
*** 577,583 ****
  				}
  
  				ReleaseSysCache(typeTup);
- 			}
  			break;
  
  		case PLPGSQL_DML_TRIGGER:
--- 576,581 ----
*************** plpgsql_compile_inline(char *proc_source
*** 890,895 ****
--- 888,894 ----
  	function->fn_retset = false;
  	function->fn_retistuple = false;
  	function->fn_retisdomain = false;
+ 	function->fn_prokind = PROKIND_FUNCTION;
  	/* a bit of hardwired knowledge about type VOID here */
  	function->fn_retbyval = true;
  	function->fn_rettyplen = sizeof(int32);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 4ff87e0..297aa3e 100644
*** a/src/pl/plpgsql/src/pl_exec.c
--- b/src/pl/plpgsql/src/pl_exec.c
*************** plpgsql_exec_function(PLpgSQL_function *
*** 573,579 ****
  	estate.err_text = NULL;
  	estate.err_stmt = (PLpgSQL_stmt *) (func->action);
  	rc = exec_stmt_block(&estate, func->action);
! 	if (rc != PLPGSQL_RC_RETURN && func->fn_rettype)
  	{
  		estate.err_stmt = NULL;
  		estate.err_text = NULL;
--- 573,579 ----
  	estate.err_text = NULL;
  	estate.err_stmt = (PLpgSQL_stmt *) (func->action);
  	rc = exec_stmt_block(&estate, func->action);
! 	if (rc != PLPGSQL_RC_RETURN)
  	{
  		estate.err_stmt = NULL;
  		estate.err_text = NULL;
*************** plpgsql_exec_function(PLpgSQL_function *
*** 617,625 ****
  	}
  	else if (!estate.retisnull)
  	{
! 		if (!func->fn_rettype)
  			ereport(ERROR,
! 					(errmsg("cannot return a value from a procedure")));
  
  		/*
  		 * Cast result value to function's declared result type, and copy it
--- 617,626 ----
  	}
  	else if (!estate.retisnull)
  	{
! 		if (func->fn_prokind == PROKIND_PROCEDURE)
  			ereport(ERROR,
! 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! 					 errmsg("cannot return a value from a procedure")));
  
  		/*
  		 * Cast result value to function's declared result type, and copy it
*************** exec_stmt_return(PLpgSQL_execstate *esta
*** 2956,2964 ****
  	/*
  	 * Special hack for function returning VOID: instead of NULL, return a
  	 * non-null VOID value.  This is of dubious importance but is kept for
! 	 * backwards compatibility.
  	 */
! 	if (estate->fn_rettype == VOIDOID)
  	{
  		estate->retval = (Datum) 0;
  		estate->retisnull = false;
--- 2957,2966 ----
  	/*
  	 * Special hack for function returning VOID: instead of NULL, return a
  	 * non-null VOID value.  This is of dubious importance but is kept for
! 	 * backwards compatibility.  We don't do it for procedures, though.
  	 */
! 	if (estate->fn_rettype == VOIDOID &&
! 		estate->func->fn_prokind != PROKIND_PROCEDURE)
  	{
  		estate->retval = (Datum) 0;
  		estate->retisnull = false;
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 688fbd6..697ead0 100644
*** a/src/pl/plpgsql/src/pl_gram.y
--- b/src/pl/plpgsql/src/pl_gram.y
***************
*** 16,21 ****
--- 16,22 ----
  #include "postgres.h"
  
  #include "catalog/namespace.h"
+ #include "catalog/pg_proc.h"
  #include "catalog/pg_type.h"
  #include "parser/parser.h"
  #include "parser/parse_type.h"
*************** make_return_stmt(int location)
*** 3137,3143 ****
  					 parser_errposition(yylloc)));
  		new->retvarno = plpgsql_curr_compile->out_param_varno;
  	}
! 	else if (plpgsql_curr_compile->fn_rettype == VOIDOID)
  	{
  		if (yylex() != ';')
  			ereport(ERROR,
--- 3138,3145 ----
  					 parser_errposition(yylloc)));
  		new->retvarno = plpgsql_curr_compile->out_param_varno;
  	}
! 	else if (plpgsql_curr_compile->fn_rettype == VOIDOID &&
! 			 plpgsql_curr_compile->fn_prokind != PROKIND_PROCEDURE)
  	{
  		if (yylex() != ';')
  			ereport(ERROR,
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index 26a7344..dd59036 100644
*** a/src/pl/plpgsql/src/plpgsql.h
--- b/src/pl/plpgsql/src/plpgsql.h
*************** typedef struct PLpgSQL_function
*** 918,923 ****
--- 918,924 ----
  	bool		fn_retisdomain;
  	bool		fn_retset;
  	bool		fn_readonly;
+ 	char		fn_prokind;
  
  	int			fn_nargs;
  	int			fn_argvarnos[FUNC_MAX_ARGS];
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index 4e06413..82cc3f2 100644
*** a/src/pl/plpython/plpy_procedure.c
--- b/src/pl/plpython/plpy_procedure.c
*************** PLy_procedure_create(HeapTuple procTup, 
*** 188,194 ****
  		proc->fn_tid = procTup->t_self;
  		proc->fn_readonly = (procStruct->provolatile != PROVOLATILE_VOLATILE);
  		proc->is_setof = procStruct->proretset;
! 		proc->is_procedure = (procStruct->prorettype == InvalidOid);
  		proc->src = NULL;
  		proc->argnames = NULL;
  		proc->args = NULL;
--- 188,194 ----
  		proc->fn_tid = procTup->t_self;
  		proc->fn_readonly = (procStruct->provolatile != PROVOLATILE_VOLATILE);
  		proc->is_setof = procStruct->proretset;
! 		proc->is_procedure = (procStruct->prokind == PROKIND_PROCEDURE);
  		proc->src = NULL;
  		proc->argnames = NULL;
  		proc->args = NULL;
*************** PLy_procedure_create(HeapTuple procTup, 
*** 208,214 ****
  		 * get information required for output conversion of the return value,
  		 * but only if this isn't a trigger or procedure.
  		 */
! 		if (!is_trigger && procStruct->prorettype)
  		{
  			Oid			rettype = procStruct->prorettype;
  			HeapTuple	rvTypeTup;
--- 208,214 ----
  		 * get information required for output conversion of the return value,
  		 * but only if this isn't a trigger or procedure.
  		 */
! 		if (!is_trigger && procStruct->prokind != PROKIND_PROCEDURE)
  		{
  			Oid			rettype = procStruct->prorettype;
  			HeapTuple	rvTypeTup;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index 5df4dfd..2eb6c33 100644
*** a/src/pl/tcl/pltcl.c
--- b/src/pl/tcl/pltcl.c
*************** compile_pltcl_function(Oid fn_oid, Oid t
*** 1523,1531 ****
  		 * Get the required information for input conversion of the
  		 * return value.
  		 ************************************************************/
! 		prodesc->fn_is_procedure = (procStruct->prorettype == InvalidOid);
  
! 		if (!is_trigger && !is_event_trigger && procStruct->prorettype)
  		{
  			Oid			rettype = procStruct->prorettype;
  
--- 1523,1531 ----
  		 * Get the required information for input conversion of the
  		 * return value.
  		 ************************************************************/
! 		prodesc->fn_is_procedure = (procStruct->prokind == PROKIND_PROCEDURE);
  
! 		if (!is_trigger && !is_event_trigger && !prodesc->fn_is_procedure)
  		{
  			Oid			rettype = procStruct->prorettype;
  
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index 44356de..3e40df1 100644
*** a/src/test/regress/expected/alter_generic.out
--- b/src/test/regress/expected/alter_generic.out
*************** ERROR:  must be owner of function alt_ag
*** 83,103 ****
  ALTER AGGREGATE alt_agg2(int) SET SCHEMA alt_nsp2;  -- failed (name conflict)
  ERROR:  function alt_agg2(integer) already exists in schema "alt_nsp2"
  RESET SESSION AUTHORIZATION;
! SELECT n.nspname, proname, prorettype::regtype, proisagg, a.rolname
    FROM pg_proc p, pg_namespace n, pg_authid a
    WHERE p.pronamespace = n.oid AND p.proowner = a.oid
      AND n.nspname IN ('alt_nsp1', 'alt_nsp2')
    ORDER BY nspname, proname;
!  nspname  |  proname  | prorettype | proisagg |       rolname       
! ----------+-----------+------------+----------+---------------------
!  alt_nsp1 | alt_agg2  | integer    | t        | regress_alter_user2
!  alt_nsp1 | alt_agg3  | integer    | t        | regress_alter_user1
!  alt_nsp1 | alt_agg4  | integer    | t        | regress_alter_user2
!  alt_nsp1 | alt_func2 | integer    | f        | regress_alter_user2
!  alt_nsp1 | alt_func3 | integer    | f        | regress_alter_user1
!  alt_nsp1 | alt_func4 | integer    | f        | regress_alter_user2
!  alt_nsp2 | alt_agg2  | integer    | t        | regress_alter_user3
!  alt_nsp2 | alt_func2 | integer    | f        | regress_alter_user3
  (8 rows)
  
  --
--- 83,103 ----
  ALTER AGGREGATE alt_agg2(int) SET SCHEMA alt_nsp2;  -- failed (name conflict)
  ERROR:  function alt_agg2(integer) already exists in schema "alt_nsp2"
  RESET SESSION AUTHORIZATION;
! SELECT n.nspname, proname, prorettype::regtype, prokind, a.rolname
    FROM pg_proc p, pg_namespace n, pg_authid a
    WHERE p.pronamespace = n.oid AND p.proowner = a.oid
      AND n.nspname IN ('alt_nsp1', 'alt_nsp2')
    ORDER BY nspname, proname;
!  nspname  |  proname  | prorettype | prokind |       rolname       
! ----------+-----------+------------+---------+---------------------
!  alt_nsp1 | alt_agg2  | integer    | a       | regress_alter_user2
!  alt_nsp1 | alt_agg3  | integer    | a       | regress_alter_user1
!  alt_nsp1 | alt_agg4  | integer    | a       | regress_alter_user2
!  alt_nsp1 | alt_func2 | integer    | f       | regress_alter_user2
!  alt_nsp1 | alt_func3 | integer    | f       | regress_alter_user1
!  alt_nsp1 | alt_func4 | integer    | f       | regress_alter_user2
!  alt_nsp2 | alt_agg2  | integer    | a       | regress_alter_user3
!  alt_nsp2 | alt_func2 | integer    | f       | regress_alter_user3
  (8 rows)
  
  --
diff --git a/src/test/regress/expected/create_function_3.out b/src/test/regress/expected/create_function_3.out
index 5ff1e0d..3cdd92c 100644
*** a/src/test/regress/expected/create_function_3.out
--- b/src/test/regress/expected/create_function_3.out
*************** ERROR:  could not find a function named 
*** 271,276 ****
--- 271,285 ----
  DROP FUNCTION functest_b_2;  -- error, ambiguous
  ERROR:  function name "functest_b_2" is not unique
  HINT:  Specify the argument list to select the function unambiguously.
+ -- CREATE OR REPLACE tests
+ CREATE FUNCTION functest1(a int) RETURNS int LANGUAGE SQL AS 'SELECT $1';
+ CREATE OR REPLACE FUNCTION functest1(a int) RETURNS int LANGUAGE SQL WINDOW AS 'SELECT $1';
+ ERROR:  cannot change routine type
+ DETAIL:  "functest1" is a function.
+ CREATE OR REPLACE PROCEDURE functest1(a int) LANGUAGE SQL AS 'SELECT $1';
+ ERROR:  cannot change routine type
+ DETAIL:  "functest1" is a function.
+ DROP FUNCTION functest1(a int);
  -- Cleanups
  DROP SCHEMA temp_func_test CASCADE;
  NOTICE:  drop cascades to 16 other objects
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 6616cc1..01608d2 100644
*** a/src/test/regress/expected/opr_sanity.out
--- b/src/test/regress/expected/opr_sanity.out
*************** WHERE p1.prolang = 0 OR p1.prorettype = 
*** 74,79 ****
--- 74,80 ----
         0::oid = ANY (p1.proargtypes) OR
         procost <= 0 OR
         CASE WHEN proretset THEN prorows <= 0 ELSE prorows != 0 END OR
+        prokind NOT IN ('f', 'a', 'w', 'p') OR
         provolatile NOT IN ('i', 's', 'v') OR
         proparallel NOT IN ('s', 'r', 'u');
   oid | proname 
*************** WHERE prosrc IS NULL OR prosrc = '' OR p
*** 88,97 ****
  -----+---------
  (0 rows)
  
! -- proiswindow shouldn't be set together with proisagg or proretset
  SELECT p1.oid, p1.proname
  FROM pg_proc AS p1
! WHERE proiswindow AND (proisagg OR proretset);
   oid | proname 
  -----+---------
  (0 rows)
--- 89,98 ----
  -----+---------
  (0 rows)
  
! -- proretset should only be set for normal functions
  SELECT p1.oid, p1.proname
  FROM pg_proc AS p1
! WHERE proretset AND prokind != 'f';
   oid | proname 
  -----+---------
  (0 rows)
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 154,162 ****
  WHERE p1.oid < p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     (p1.proisagg = false OR p2.proisagg = false) AND
      (p1.prolang != p2.prolang OR
!      p1.proisagg != p2.proisagg OR
       p1.prosecdef != p2.prosecdef OR
       p1.proleakproof != p2.proleakproof OR
       p1.proisstrict != p2.proisstrict OR
--- 155,163 ----
  WHERE p1.oid < p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     (p1.prokind != 'a' OR p2.prokind != 'a') AND
      (p1.prolang != p2.prolang OR
!      p1.prokind != p2.prokind OR
       p1.prosecdef != p2.prosecdef OR
       p1.proleakproof != p2.proleakproof OR
       p1.proisstrict != p2.proisstrict OR
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 182,188 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.prorettype < p2.prorettype)
--- 183,189 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.prorettype < p2.prorettype)
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 198,204 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.proargtypes[0] < p2.proargtypes[0])
--- 199,205 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.proargtypes[0] < p2.proargtypes[0])
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 216,222 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.proargtypes[1] < p2.proargtypes[1])
--- 217,223 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.proargtypes[1] < p2.proargtypes[1])
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 233,239 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[2] < p2.proargtypes[2])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
--- 234,240 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[2] < p2.proargtypes[2])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 246,252 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[3] < p2.proargtypes[3])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
--- 247,253 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[3] < p2.proargtypes[3])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 259,265 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[4] < p2.proargtypes[4])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
--- 260,266 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[4] < p2.proargtypes[4])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 271,277 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[5] < p2.proargtypes[5])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
--- 272,278 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[5] < p2.proargtypes[5])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 283,289 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[6] < p2.proargtypes[6])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
--- 284,290 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[6] < p2.proargtypes[6])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 295,301 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[7] < p2.proargtypes[7])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
--- 296,302 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[7] < p2.proargtypes[7])
  ORDER BY 1, 2;
   proargtypes | proargtypes 
*************** WHERE aggfnoid = 0 OR aggtransfn = 0 OR
*** 1292,1306 ****
  SELECT a.aggfnoid::oid, p.proname
  FROM pg_aggregate as a, pg_proc as p
  WHERE a.aggfnoid = p.oid AND
!     (NOT p.proisagg OR p.proretset OR p.pronargs < a.aggnumdirectargs);
   aggfnoid | proname 
  ----------+---------
  (0 rows)
  
! -- Make sure there are no proisagg pg_proc entries without matches.
  SELECT oid, proname
  FROM pg_proc as p
! WHERE p.proisagg AND
      NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid);
   oid | proname 
  -----+---------
--- 1293,1307 ----
  SELECT a.aggfnoid::oid, p.proname
  FROM pg_aggregate as a, pg_proc as p
  WHERE a.aggfnoid = p.oid AND
!     (p.prokind != 'a' OR p.proretset OR p.pronargs < a.aggnumdirectargs);
   aggfnoid | proname 
  ----------+---------
  (0 rows)
  
! -- Make sure there are no prokind = PROKIND_AGGREGATE pg_proc entries without matches.
  SELECT oid, proname
  FROM pg_proc as p
! WHERE p.prokind = 'a' AND
      NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid);
   oid | proname 
  -----+---------
*************** ORDER BY 1, 2;
*** 1639,1645 ****
  SELECT p1.oid::regprocedure, p2.oid::regprocedure
  FROM pg_proc AS p1, pg_proc AS p2
  WHERE p1.oid < p2.oid AND p1.proname = p2.proname AND
!     p1.proisagg AND p2.proisagg AND
      array_dims(p1.proargtypes) != array_dims(p2.proargtypes)
  ORDER BY 1;
       oid      |   oid   
--- 1640,1646 ----
  SELECT p1.oid::regprocedure, p2.oid::regprocedure
  FROM pg_proc AS p1, pg_proc AS p2
  WHERE p1.oid < p2.oid AND p1.proname = p2.proname AND
!     p1.prokind = 'a' AND p2.prokind = 'a' AND
      array_dims(p1.proargtypes) != array_dims(p2.proargtypes)
  ORDER BY 1;
       oid      |   oid   
*************** ORDER BY 1;
*** 1650,1656 ****
  -- For the same reason, built-in aggregates with default arguments are no good.
  SELECT oid, proname
  FROM pg_proc AS p
! WHERE proisagg AND proargdefaults IS NOT NULL;
   oid | proname 
  -----+---------
  (0 rows)
--- 1651,1657 ----
  -- For the same reason, built-in aggregates with default arguments are no good.
  SELECT oid, proname
  FROM pg_proc AS p
! WHERE prokind = 'a' AND proargdefaults IS NOT NULL;
   oid | proname 
  -----+---------
  (0 rows)
*************** WHERE proisagg AND proargdefaults IS NOT
*** 1660,1666 ****
  -- that is not subject to the misplaced ORDER BY issue).
  SELECT p.oid, proname
  FROM pg_proc AS p JOIN pg_aggregate AS a ON a.aggfnoid = p.oid
! WHERE proisagg AND provariadic != 0 AND a.aggkind = 'n';
   oid | proname 
  -----+---------
  (0 rows)
--- 1661,1667 ----
  -- that is not subject to the misplaced ORDER BY issue).
  SELECT p.oid, proname
  FROM pg_proc AS p JOIN pg_aggregate AS a ON a.aggfnoid = p.oid
! WHERE prokind = 'a' AND provariadic != 0 AND a.aggkind = 'n';
   oid | proname 
  -----+---------
  (0 rows)
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 5acb92f..d7eff6c 100644
*** a/src/test/regress/expected/rules.out
--- b/src/test/regress/expected/rules.out
*************** UNION ALL
*** 1521,1529 ****
   SELECT l.objoid,
      l.classoid,
      l.objsubid,
!         CASE
!             WHEN (pro.proisagg = true) THEN 'aggregate'::text
!             WHEN (pro.proisagg = false) THEN 'function'::text
              ELSE NULL::text
          END AS objtype,
      pro.pronamespace AS objnamespace,
--- 1521,1531 ----
   SELECT l.objoid,
      l.classoid,
      l.objsubid,
!         CASE pro.prokind
!             WHEN 'a'::"char" THEN 'aggregate'::text
!             WHEN 'f'::"char" THEN 'function'::text
!             WHEN 'p'::"char" THEN 'procedure'::text
!             WHEN 'w'::"char" THEN 'window'::text
              ELSE NULL::text
          END AS objtype,
      pro.pronamespace AS objnamespace,
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index 96be6e7..fd43f23 100644
*** a/src/test/regress/sql/alter_generic.sql
--- b/src/test/regress/sql/alter_generic.sql
*************** ALTER AGGREGATE alt_agg2(int) SET SCHEMA
*** 81,87 ****
  
  RESET SESSION AUTHORIZATION;
  
! SELECT n.nspname, proname, prorettype::regtype, proisagg, a.rolname
    FROM pg_proc p, pg_namespace n, pg_authid a
    WHERE p.pronamespace = n.oid AND p.proowner = a.oid
      AND n.nspname IN ('alt_nsp1', 'alt_nsp2')
--- 81,87 ----
  
  RESET SESSION AUTHORIZATION;
  
! SELECT n.nspname, proname, prorettype::regtype, prokind, a.rolname
    FROM pg_proc p, pg_namespace n, pg_authid a
    WHERE p.pronamespace = n.oid AND p.proowner = a.oid
      AND n.nspname IN ('alt_nsp1', 'alt_nsp2')
diff --git a/src/test/regress/sql/create_function_3.sql b/src/test/regress/sql/create_function_3.sql
index fbdf831..8f209d5 100644
*** a/src/test/regress/sql/create_function_3.sql
--- b/src/test/regress/sql/create_function_3.sql
*************** DROP FUNCTION functest_b_1;  -- error, n
*** 175,180 ****
--- 175,188 ----
  DROP FUNCTION functest_b_2;  -- error, ambiguous
  
  
+ -- CREATE OR REPLACE tests
+ 
+ CREATE FUNCTION functest1(a int) RETURNS int LANGUAGE SQL AS 'SELECT $1';
+ CREATE OR REPLACE FUNCTION functest1(a int) RETURNS int LANGUAGE SQL WINDOW AS 'SELECT $1';
+ CREATE OR REPLACE PROCEDURE functest1(a int) LANGUAGE SQL AS 'SELECT $1';
+ DROP FUNCTION functest1(a int);
+ 
+ 
  -- Cleanups
  DROP SCHEMA temp_func_test CASCADE;
  DROP USER regress_unpriv_user;
diff --git a/src/test/regress/sql/opr_sanity.sql b/src/test/regress/sql/opr_sanity.sql
index e8fdf84..a593d37 100644
*** a/src/test/regress/sql/opr_sanity.sql
--- b/src/test/regress/sql/opr_sanity.sql
*************** WHERE p1.prolang = 0 OR p1.prorettype = 
*** 82,87 ****
--- 82,88 ----
         0::oid = ANY (p1.proargtypes) OR
         procost <= 0 OR
         CASE WHEN proretset THEN prorows <= 0 ELSE prorows != 0 END OR
+        prokind NOT IN ('f', 'a', 'w', 'p') OR
         provolatile NOT IN ('i', 's', 'v') OR
         proparallel NOT IN ('s', 'r', 'u');
  
*************** SELECT p1.oid, p1.proname
*** 90,99 ****
  FROM pg_proc as p1
  WHERE prosrc IS NULL OR prosrc = '' OR prosrc = '-';
  
! -- proiswindow shouldn't be set together with proisagg or proretset
  SELECT p1.oid, p1.proname
  FROM pg_proc AS p1
! WHERE proiswindow AND (proisagg OR proretset);
  
  -- currently, no built-in functions should be SECURITY DEFINER;
  -- this might change in future, but there will probably never be many.
--- 91,100 ----
  FROM pg_proc as p1
  WHERE prosrc IS NULL OR prosrc = '' OR prosrc = '-';
  
! -- proretset should only be set for normal functions
  SELECT p1.oid, p1.proname
  FROM pg_proc AS p1
! WHERE proretset AND prokind != 'f';
  
  -- currently, no built-in functions should be SECURITY DEFINER;
  -- this might change in future, but there will probably never be many.
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 140,148 ****
  WHERE p1.oid < p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     (p1.proisagg = false OR p2.proisagg = false) AND
      (p1.prolang != p2.prolang OR
!      p1.proisagg != p2.proisagg OR
       p1.prosecdef != p2.prosecdef OR
       p1.proleakproof != p2.proleakproof OR
       p1.proisstrict != p2.proisstrict OR
--- 141,149 ----
  WHERE p1.oid < p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     (p1.prokind != 'a' OR p2.prokind != 'a') AND
      (p1.prolang != p2.prolang OR
!      p1.prokind != p2.prokind OR
       p1.prosecdef != p2.prosecdef OR
       p1.proleakproof != p2.proleakproof OR
       p1.proisstrict != p2.proisstrict OR
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 166,172 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.prorettype < p2.prorettype)
--- 167,173 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.prorettype < p2.prorettype)
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 177,183 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.proargtypes[0] < p2.proargtypes[0])
--- 178,184 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.proargtypes[0] < p2.proargtypes[0])
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 188,194 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.proargtypes[1] < p2.proargtypes[1])
--- 189,195 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      p1.prosrc NOT LIKE E'range\\_constructor_' AND
      p2.prosrc NOT LIKE E'range\\_constructor_' AND
      (p1.proargtypes[1] < p2.proargtypes[1])
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 199,205 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[2] < p2.proargtypes[2])
  ORDER BY 1, 2;
  
--- 200,206 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[2] < p2.proargtypes[2])
  ORDER BY 1, 2;
  
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 208,214 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[3] < p2.proargtypes[3])
  ORDER BY 1, 2;
  
--- 209,215 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[3] < p2.proargtypes[3])
  ORDER BY 1, 2;
  
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 217,223 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[4] < p2.proargtypes[4])
  ORDER BY 1, 2;
  
--- 218,224 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[4] < p2.proargtypes[4])
  ORDER BY 1, 2;
  
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 226,232 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[5] < p2.proargtypes[5])
  ORDER BY 1, 2;
  
--- 227,233 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[5] < p2.proargtypes[5])
  ORDER BY 1, 2;
  
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 235,241 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[6] < p2.proargtypes[6])
  ORDER BY 1, 2;
  
--- 236,242 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[6] < p2.proargtypes[6])
  ORDER BY 1, 2;
  
*************** FROM pg_proc AS p1, pg_proc AS p2
*** 244,250 ****
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     NOT p1.proisagg AND NOT p2.proisagg AND
      (p1.proargtypes[7] < p2.proargtypes[7])
  ORDER BY 1, 2;
  
--- 245,251 ----
  WHERE p1.oid != p2.oid AND
      p1.prosrc = p2.prosrc AND
      p1.prolang = 12 AND p2.prolang = 12 AND
!     p1.prokind != 'a' AND p2.prokind != 'a' AND
      (p1.proargtypes[7] < p2.proargtypes[7])
  ORDER BY 1, 2;
  
*************** WHERE aggfnoid = 0 OR aggtransfn = 0 OR
*** 804,816 ****
  SELECT a.aggfnoid::oid, p.proname
  FROM pg_aggregate as a, pg_proc as p
  WHERE a.aggfnoid = p.oid AND
!     (NOT p.proisagg OR p.proretset OR p.pronargs < a.aggnumdirectargs);
  
! -- Make sure there are no proisagg pg_proc entries without matches.
  
  SELECT oid, proname
  FROM pg_proc as p
! WHERE p.proisagg AND
      NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid);
  
  -- If there is no finalfn then the output type must be the transtype.
--- 805,817 ----
  SELECT a.aggfnoid::oid, p.proname
  FROM pg_aggregate as a, pg_proc as p
  WHERE a.aggfnoid = p.oid AND
!     (p.prokind != 'a' OR p.proretset OR p.pronargs < a.aggnumdirectargs);
  
! -- Make sure there are no prokind = PROKIND_AGGREGATE pg_proc entries without matches.
  
  SELECT oid, proname
  FROM pg_proc as p
! WHERE p.prokind = 'a' AND
      NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid);
  
  -- If there is no finalfn then the output type must be the transtype.
*************** ORDER BY 1, 2;
*** 1089,1095 ****
  SELECT p1.oid::regprocedure, p2.oid::regprocedure
  FROM pg_proc AS p1, pg_proc AS p2
  WHERE p1.oid < p2.oid AND p1.proname = p2.proname AND
!     p1.proisagg AND p2.proisagg AND
      array_dims(p1.proargtypes) != array_dims(p2.proargtypes)
  ORDER BY 1;
  
--- 1090,1096 ----
  SELECT p1.oid::regprocedure, p2.oid::regprocedure
  FROM pg_proc AS p1, pg_proc AS p2
  WHERE p1.oid < p2.oid AND p1.proname = p2.proname AND
!     p1.prokind = 'a' AND p2.prokind = 'a' AND
      array_dims(p1.proargtypes) != array_dims(p2.proargtypes)
  ORDER BY 1;
  
*************** ORDER BY 1;
*** 1097,1103 ****
  
  SELECT oid, proname
  FROM pg_proc AS p
! WHERE proisagg AND proargdefaults IS NOT NULL;
  
  -- For the same reason, we avoid creating built-in variadic aggregates, except
  -- that variadic ordered-set aggregates are OK (since they have special syntax
--- 1098,1104 ----
  
  SELECT oid, proname
  FROM pg_proc AS p
! WHERE prokind = 'a' AND proargdefaults IS NOT NULL;
  
  -- For the same reason, we avoid creating built-in variadic aggregates, except
  -- that variadic ordered-set aggregates are OK (since they have special syntax
*************** WHERE proisagg AND proargdefaults IS NOT
*** 1105,1111 ****
  
  SELECT p.oid, proname
  FROM pg_proc AS p JOIN pg_aggregate AS a ON a.aggfnoid = p.oid
! WHERE proisagg AND provariadic != 0 AND a.aggkind = 'n';
  
  
  -- **************** pg_opfamily ****************
--- 1106,1112 ----
  
  SELECT p.oid, proname
  FROM pg_proc AS p JOIN pg_aggregate AS a ON a.aggfnoid = p.oid
! WHERE prokind = 'a' AND provariadic != 0 AND a.aggkind = 'n';
  
  
  -- **************** pg_opfamily ****************
v3-0002-data-line-updates.patch.gzapplication/x-gzip; name=v3-0002-data-line-updates.patch.gzDownload
�K�Zv3-0002-data-line-updates.patch��k����&
���2��[�R/�����J=s�V+��%;����4$�dB
,���9��'.���x<�5�cR�%�?������c�<?;?�����+��wI�MO���mTEi������|�������]���/����������=�������f����>~�xO����{W���9�y���s�m��~\��]���c^~����/9��?i������;��_9��w?:���Y��_}�~��w���o�I�2.*���?8�?��o�)��${xp����?�3��/��g���=�����S��{K�����MvJ������������w�>:c���}��o���������a"�j~�P&9R���TF�d�Tb���S"s.��UqDp�bz)%�g�}2��k��=s�uM�AxJ���p�/QAp�0��z&��6��t����^��3N�J���s��>��.e�Lb��aS!r�@���Z,��<s�T�9�$�|�O�`w)dCg�
��*�'}O,��<s���������y��	����qJ�i�
��9h�$g�A���R����B�_��S"j����Y�1l*D
TzD
������3N�J�����<������|+��h�
��^�A�s�#�q�A�x��`�~P�������yA'�h^C�6�^9B�4�wk���Z�+�NE1��|�2BH8�y����K�S���$	 �I`������W
Tz���<s�T���*�Q\�."@!l�������
{��q��wG�1l
D.�����9�/�7b�L!S!���jxc���F�1l*D�a����������6S�THj���V��{#��6"�$Ov���`��'$Fi�
��3����`���}�i�
��fkK����~�Q���nIo���j��m������^�����k����BX������,��Veax�\�O��{�@��7��-����������8�d<����Ge���NQP�[8�,�^X��BG}���Y�.����`��uJK�n��'�(����1��B���<�������2m�+�Eg��4������M�V��
��({��� l x@�4Vy2�Ho2�wusx��������]���d���f�&swW7w7������������Mf��n�n2gwusv�������]�|�d��jf�suW7W7�������<����Mf��n�n2Gwust�������/7!�7�$�kl~6��G���M��*h0Vl��]�������8��0��&G-�}���pur{���~���Nk,�3�,�[�	O���C�����"��3�$>	8q>��1�3��u�L����c����4�
����n6�4��p�q'h4��L�#��������E���F�^X���Q���tb
����y3CG]�������"J���Lr���&P	�q_~J7�,�C��d��g�$wM{�����E��������U�d��43\:\��~����m~8D�n�7�!�����W� ��xm���c)\
���-a>?�1���
^��!�E�}��;�(
pY�D�v��|=�_�����D�.	"�
�����f3������6+p��2N��}�~��y������fC�8,��p�@��^����V������5������f�s�8��x�������(�R���Y6FF���
N{���D�T��(�S�\WK6��
#��W�@��q��B����������c`o�f{��`���*���L`������S$�-�)����Z
�lqc7��n���c��X'��Fm�-�������t)��;`�i�	b`�����{�)���-#��3�7����~���# ��W%e������o�=�h0b{xUr�	������z1%`�������V\�;�����fE�w�~����	�=����S�S������R���-�� �BT8��y�bje���Q���P�������.j�m�{$f�T�Lm�����8J�����\�������xC�5���Oq!��mH��
��8�?����m��j�SwN�3:��"m>�^���}�r�T�i��d�M�S	�a�\
������"T!Ud)i�IN�\AP	��D�r%������:^��-�I|*����U/$���UI! �>��T��[�������Dh�
�A��j&�4����<s��.3k�j��zNG���g�
�m6�R�"����$��N��+)�G�G��Z���0l*@��[7S;�e��`����`�!O�J)�}1h&a#6`\!$�@�h?WV�rI ���Y$���
N!	:���Y+@�$���R%n
�h �K�y�9>=��5s!��j�����W�����fgU\���9|��� E�q��Z������<�x���gi&s4W?G3�����������Lfg�~vf27s'���gf�$������$/3����de�s2w���lF�jgd�1w�|�l6�N����\�\�N&vo��s^����I�����xyQ2�9`s_YO.f �e����hVj�[�N)\>$@k^2!��e��2��s`��
K����O�[��5�L���;���y[Poo��^����a\tT��'*��h(<y_���<tc�^4��a\^se�O��
�����.y5��	������������,�7����J�|x^�k��Q;^����.��cN�#�������E�w�D|�~s��)o9k��]���.�.���'��������H�����V��	so�?_{��w��/�-
>��o��_����r��_�����co�?4=]v���(�U�P�50�z�� 3/�8������p��z�,�G���c�`>�?�&n� �����(x���a|]<�h�(��������^7��X�>^�8>�]���~D|`��k���^D�t���m��3l.S�N ���9l���!����1�{��yVEIfe-	v�e�;p�-3���p}�����t>������7�X	���^V������hJ��^c����upvs�*����;xQ����]���8����Z�9V��/��wO-��W��Y+���~���
��x����9��E��E�����H/{����Z��q����n�0������ar�������
Ww�������
�[7\��&7n��7Ln�pu�m�����o�0�e����ap����a��v
W������f
�[5\��&7j��5�n�pu�i����n�0�E����at����A���Ww{�������[3\��F7f��3�n�pu�e����n�0�%����atC�;����1�	�c����N���Vw��&7b��1�m�p��a�����o�0�����ap������W��������[/\��7^��/n�p5�]�t�N�����w�-�7\�l�0����f�����D�-,l�p��jax��;�F�,���Y��d���7YX�b��/��m����B:e����*�)�+,l�������7ns�O0���<���;���#)PX���w�l�/����"W�^)F�VH)��(BR�0���p��"��V����w���	�{�0�>��>\�|�E��r�3yb��	r��<H�x:�W������I!��M��2���3�g+��JR:�Hd:�S�!�Q�� 
�A�1�pr'�@��;E�1}�i�����$E�~_��������2��O��x8�.�;��u����S�
�c�����~.gB?�ct{�t�<Dij��ZF8�
�
�����#W�Ynqf�%
��4���}�lbMy%
� o�Xs�Y3����u�JH�'��y��'A���0����*��'���<	��(BR�l0$��x��������.+I�$$��<��������<����<�eqp����pC�<����! �.)
I�$0Bx���fB�y$*Q�I�Hd:�S�!�Q�� 
�A�1�pr'�@��;E�1}�i�����5O����6?<%^�������VK�A�l��9�W������C��)�P��X- �����h�:gE��4gEAk+:g%�
L�x�\�C�{QM��0�\������6�tb@�r/Q��i�T����=��Y{�������I�m��#�[���"?e���H)�Cg[{w�x���,��=^|j6f]�h�%���`��av��]�1l
��1����
�*�6NR��r�4S�J/���/���F�)�U�:���l��1������,O��N+d�^��3m���|�[��F��3��r �4S����?�r�;�*���SQ�f���rhy��n�db3N�c�������S����0qL{3�m�����5W��z���i����H��TD���36BJ�������oG��������3�N��X�)d�����"�^���6%�8U�(�W`�4�!d
��<{����>
��Fy���}��RG���a3��&��Q^�J!>i'��r���dAf�%��G�1�?�i����Ve������t~�S1���f��U�mcJ���C���6c��L@k���h}C�SY%�X}�d��0������1E���OP��l�,/6(�fx%��,Fb�h��S���/�b@ZQ�F���kxk�`��Fi�
�b��D�O�-��"�)�+���7	�����S���-^�T�f��A!�@zr@��Lm����������������Wg�yL��9�8P�0U�m�+�	�XI����>j$�!j}F/�<�)�@�����
`B���NC�a2�u�V!6`�s�1�����'2>u�fM�UlmB�0���v.�TqV9;�#"��N���7��at�z�����`��tgh���K\����J��P_^�R��V�P�������~yo�E��si��)j�e�K[(�(�Uo�Gm����6�PR�8�n��Jj�Q�m\�S����m��6�FE[G���?���m��v�6���)����G(�=��r~v[���E=tq>�{�4Or/��)�k������Bwu	��a{p��//��!�,�������&\�X�����h o�9S�&\�c���t���
�np�����$�y'�k�@�������]�!����?`6>\v�������`��P��*(��e��dW�l|������]�!����?@6><v������D�cCcw������;�������fXlgP�N3(�3$v��������i��v��4�a;Caw������;�@��0�Yf���N��h��v#W����TW�_��q,�	 �(<S�n��d<o��%�U��NE��e��HT�*G������I�T1z��l!���Dicc������:^���"8.6%\>U^���Oh��Sja�1}"a�g(f�1}��qe�F�F]�V�j��
{wuiT�����}�^|zm_'�s:���O���>������NW
��]a���\��4���A�����.��O��=A#h������g^�������h�|P��Ro������]�]�:����*�����-��.<�{����7�o�Q��j�����h�^��j�F1�-{��q�-���^w�Z�n����{K<�-�WR~o�GB���zt:m�<D���Q��1�;������p��o�I���G���[�c[w������;�������d\k|T�N1�5=�u����GLlDkz<�j�gM�f]�������=�5=�u�G������8��(v�5c�X�cXW{kz�j�`M�_]�������=z5=vu����G�#���:�|�+�G'`_�����^��Ot�u&�!��|U0��qt��!���U0�rR[\�(�A���{��y�-.�(��1���4�p��ZS0�rt��!�p��Fs�Ii%���>J[\�m�wG�|o�K��{�>J[\�m\���9�U/�����9�=�[��6��j��}���P0�;�=��~��������~�n�Ow��t�=������]���xo������rW�/7����=��~��������~/n�w'��m����k����O����||�9��S�~�u^����Et��x���!����!(6C����y{H����������b3�l������^Po_�y�����d�� 3�k�N�j�����!��V�9|{���F��xK���(�������
�SA��N\]i�~�������2\7oo��f_��Y�Z��T�nz�8�k���n������&����n��4��.U����/N���e��������qQ���!����OU������Z���Z��P-�ie`���-�"���l�9����%*_m�����a4����?Lb\$�{6�3�xO�8��0��q�0��@j���(��p�Y���n�����y�;! �D��K��dS�OE�>������F��~����Z����o������&�?o����w���n#�YC�M�}K���o��Ei��N��2�}����4v��>.G�%7r]�/"^ZG�R~OW��Q���0;Q���=���39a$$�b�SR���23�����X��y������8:���x�g��A�R)�w�>�g]�s�$k
b���g�Fe9��R�~�oD�6l��9���M�����/_)8?�H�L��"�����e\���3��@:0�����F�����D�����E�w
5j��L�m�������	���)���*����&?w����1O�6��-����Y*_ma[�m���m�U����Z8kQ T������B� ����_�B ����
�����uB�����p��V\��sq����c�Y�;���e��5��� �UU����u�%tqr-L���o8����|���"���D��B�w�
�6B�;I�7�����0�N��yW?���/n�=�R�S�
��c��P'��wA���iQoP�;���#����b�Q�����H�1E����e�Q���qT��y>�T���������~}7T_��n&��]�O�O��.���S};P����}�h!���'�i��=��6���o�2��=S^:j�@T��"�����Jh��C
�&���r���;E�����p�I�7�8L����i��m���4��I�mA��}�s������z�@o���I���s������uh[�!o_J'��5mY:j�/Z)���M����\�
��-��z6�
X����y�1����nS����$��������Q����{������/%�v��7h�E�!�j(��P��}�
b��B�����T��"$p,7�VY$���������b@�Q�FR����t��mu����^��\$�[���U���&��;1�0F�q���U��m���,�&���o�+a�Q��!��j=s�JJ4r0�Q�f���m��S��/P�Y�/
���<P���T{!6md#o3�����0�u
\`	��k'QQDo���_T�Q5��@�a��q�|�=�O�$�<���h����*��o�xs����(�l,�������$����?��������_��!�J�IvX���,d��i��(5G�NaS�ivXOcOG)��z
;��L���6�e�	���}J\A[�L��7�������f;x:J�q	m2���C��1A/�NL����>�IV����"k��6�Ye>���
�4���W���^v:�E�m~GM���X��>���b'-��f���0���e��������a$�~�V����K^i[���i\���|��:S�Y�!�;�O�q�B,�=b�B���i^�K��n�Fr���2@@�L���
�A��k�y&lM:J�lk)�������E3����V5�/D�u�F#�@��@�~mu��$�+����k.I�B�A6r��~B�a#�p�I4,��~�a6�p���)���b�M0�	���;Aza8�p'H.��vja0�p'J,l��$i�����N*��)����w��\\�>�q�:�����}g��6��7��Y�����A����A��%�o��&��_���L�1�K=j��!���.�v���d�.����gVt����2���>X%��&C�j���a�dV��1��gV�Q8r��e
s
)��h����!b����+�6����k
'�������`�6���w�~�}m��y��	
���]@cTs����8��"g����\��A�[Q��6��p-�J�����6@`�[FPx���p�/�`�~��0��-	�
�
\��ZH�[6�{�g�R���`�~;]!$�[
\`��m�Y4�|RnvUG^_3�]�I�J�#nl��0�����^L�l	��'�g
�&�(����c4�P�����[[�����A��$��+yr��dGJ%������1p��)�������<b�8B_����*yl��(�g���2jx��g���.�`���"�p&
s[
\`���msx?Q�ke��L$�8%�]����v����(�o)pm�U�z�NC�E,B��P�
/�����S����>��@\�L���6�*����x��4�"��������.��,FfW+�3u���#�q)a���f%�^��vYR�����K1�Wp�E����+�T�f���B��T��P+�����&��I()�,P���V+�qF����K��os���\�����"n�^�~s���nG����>���`���4
Q����T�f�����95
�-�J+�����4.�k`2�(>s���x�GK2���n\���
Z�
Z�h��Sr
d��v����0J_g������{-�j2r���r���%�,P��h���4�,H%��.A���WsB�]���K����eXko��=�}6(����3[-?8����7�����ht�oZa`�&"5����}�*�z�9���p�g��������@�U�����*��b�,P�g��p
��e	���kk�y��8�����-�@������0L� ����9�z�9�v]���C��6X��4�@M�l+��vL;��}�o:����i���\��U���m�sYA��|�b_Z��C�7hO��XO�^�|��
�)\���,����
�Cb�!�-Zx���J����K^��cB@|�_�I����r_�	�6����,��xie^������eo����!�
���qWm���o�0�64�y��T�
�����y�����w���yZY�7�66��6���7���a_��;�l��[p�������6��"�F"�yn���D��CT�b�C�Wbn���A��@\������?�08�puG&���������y�w�����Ww�ar����9L�8�ax����x��om���
�c
Ww�at����4��3\�q��Q�p��Ff���D��*��&��6P����9��� `f`
�9��G�}KSdTbU������V 6C���T��5��S�Z�*OPG
4u"\����2z���'��dM=���K����:�K�m��W���ST����@!�4
Qi��yC?��/:�����i��aV���\tQqN��3(.>c�b3��f��,��������2C�#��<bE
��8��hbeP1`f`�����������*psR�X|�w=t)+}���zo�������</��m��Axm���
��({a�{{����uf8;�5
�
��m&����,L6
W�Q�l�n�0� �{���5@t7�_���J8	��(�clz�h��SY���7����I�l_����ykz�
Q��*��|����8���0����\$�v�6�����k+8�<.�I��
��*c���s]�i��U���X���m�E#��i'��I.'~���M�Q
_�9�#F��A�&��(�8R�8�9���oMu�,&\����==9X*)������Lt8#���-��kV�F�?��	n�]�E����%6�a�\�^���h^��t������n��$��pw'i��[�;I7���I�����[%\�U��\%������?���r�j��d���w�)a���"��
��V@������X����w�{��m�AG���loY;
����Q�h/d@=��k�,�.�`� o�&oY^��e,�Vq��#)R�#��7��B��qH�id������jV�W�
�8|�U`W��&��S\�7�;H1��4��a�H�1��X_/	��i�U�\���#�`��m��{�o$�%N�0b4���{�j��y��zNs��4��q������q�b��r|�{Hy� T;@[�OOe�T'K�yQ%y����U�d��]>���B%u���m�|g \{`Um0�����]RV�#\�C��[J����C�l\!�z�W�'D?t��@b�B\�K*��b�����<}#�\y���:J!
*o�?e��F�\��0m��z
H�JmB�G���_�P���N��%�@g}OM����3�S�(���J�����W���t����\]��������{����g�?���J����D��fb�$�l����fW?�0�^�����w���Fj�N�ZO,�I�i�;MZa>�p���XRa0�p�S
�	���PL'\�t�`21�yx2aa��~�O�e�������,�$���B}��
"��I�����������H9�8�z�����"e���f�-���`�u(��GJp�^����r��z\Z�x�Z|����=:,_u(w����M.���0����tr'C���;A2��	B���N�L�w��c:������=b"J��g���J�h����o�~JOH2��,����
U���k��g���\��ev��Nm
�� �@-�t����Z�z�����20�+w�O�,������^7���4�8@�.	����8���,n#�8��$�t<Z�\HB)���yN�|�.��j�������#O�B�S��Gz��7�|{�t�6*vI�I�f>��	�2QXP����2F���HE�Ud;���~�t�<����0��p��m����i��-t�����&#���S�X��t�S�-Alnc���3c<9��s����6RJG�l������,�In��.���f�5�:�������aV�)Y5�i��qdA���irH�x'^�E�r����������&M*�>$�{3����F�
���������������g#9��B�cg��.`����s�#9���,5Z���-�����LV|���� ����s>��K�k�DFm	3�M9��{���^/��n�&�A�K�)p��E.�^x��)��h�"�K�I�-��Lzx{o1���	?W'�m���C�
�!���_����J����)=f��(=�,�Y��!,�$�����M#��QQ��	ulc	9`$����%�.�2n����%��X���s�5�����a��F+�1����/�[����`9���+���}�Je��e�S
x�r��!���t���~>��{��J^�[���e�z@�&�wI���5�%�:6�M�<',���B��D!	4�4���w����KW��F��1��pI�=�V����C�K��������E��N�5�����,����Ko$n�e~��)[�qG��9���$�������?��K�������klm�J�W�x���.�\��Tk+O(lc�k��Y0y.�C����&��zL�m����suq��P�DD��@w���6O�o�E�^w��u��mHM�r���D�P{"�PFn	v����E��s��9��|����m&A0?���/�����P���H�0t��`_F��`���j�����Z^���F
���t8��fP�YC:8��Z!���z��N�����#���e�&���m��bp��`U�X�4��4���1�Qu�D5m�*@�oL�`�����F|��F��N������d���O��OqE�d�l���l�!@��1�|���d����#�}�3�8e^����s����-�F����76�����c�h�<��x��� l������y���q��a5kvU��y�y{I��K��"KP�L�|�E��0�o�}��^���^�V����_��?%��Jy�s�7Hx��_s�������{��}�����|c>� j��	Kq��<�~�=/������I!�Y)�9p
���^��~���)�������S!�o�������pe��ay�Ix�.!���}�X����Qj��������3�(@��nP;O�Y�o�/l�J��s�
MNR+zoZ1 �(F��6:��_Y����F=xE&�oUP�����\v��H��H�q���2����q!)	��i� ����D����'���rBRm�PK)������*������
�zw5e�Eyc'dFi#��{%����%n���S�|`	m�^�f5|���|�����
�G�Q��9��k��Ord�a���<����J���Jwt^&�����Z���$��\�8�tw�$@�2�+�/�}]���I��������5�h]�0<g����
��9����U��Vo����y-����[h����Bkw'h���;A[���{�2��R�P^�<��;�,.�������n��j�����gL�5_Q�s.������K^��m��p�����S$�3�%���o�9�
Wp�r��N�6�`,*����7��C�%�$�P�A|m���,3?=��V�Q��	��i������0z-�{;2;�� �)x4��;	�F9i�r�%�%�
�_��<�0h�UE����e�T+��	�3�����=p�1h��)l�U��
�{8� ��hv�����j�`����n�F[���z��]W��m�c=�!�������V�)h�[73;I� ��I�IX4���7�-�O��KT�;���g(���6R�#�,����W�Q<]���o�oGW�B����nU�ST����m�@���^�<],s����N����>$���t�q��y�W�?��o*�p����NEi
c��>��l�oN�%��*^.R|b���`����)*�3b��|��0��[�i�&��;'�m�l_����ip�@^���sC���_s�3a(�����5��0������0������];pa(��%�>����pW������wp���]���AL��Q-�����0����G�1����8l�&~�
��
�-D��/����v�wi��0Dx�W�����a�_����{;,�s����|��{����?������\�A1v��!������6C���&�M��W�!��m��p�~�t'����cU#Uw����q�;�8��(��l�ja��N6F�0Bu'�Z����O-�N��F����dcS#Sw.8258.u�Gy��R��R%������������#R��QW{<jx4�j�F
�E]������;�H��8��`jv�N5
�1u�������@��?����XO��v8���|4�y��ag�����I9�A]�^]`���-Z�f|B�$Jm�NB�I:�ey6/y{������)���t���U"}�,HS<�Z���V���"��N��H������La������RmP�N@�iB/7�Iw����'��R<�@Z���5HHdC MC���1c[@�-�ir
�����3���a1ow�^_�^`q�B��G�d��
����_����������k�`�|���3���=tJOc��z����Ky�TzD*��6@��_6�}B��S.8��&�A|+������g�����BSwE9�'NY�'AB�2����>��3��W��4�?F���9����j�W�������3m�\
k�c�1�4���8���
���\�/�,������1��y�w�i^�pym������"4�o�^��(�um�"��;��?���
��_�Q|���WF���R.�*&���i�_��� �8H�xg��e���2�B(�I���<�f(d�4�8�����2z����|y9G�����	j,�I�6��R�|�^{J�oG���R`��
��	���R@��7��w����[���$���`
H�Q�8�kY1�}����x2���>��3�n�����x��z��s��A<2� �B�I�����f��:Uq7��r<L�Z�;�y�
���}3�����R�4��63�6����{��3�.�0o���"?l���8rW3-�D_d\`��'��f��sK�k��km�m��7�0�l�I�-#	��Z�g\w-*�3>�B-��eE%���UA}P�)��(��>A����c=c���ykq�����_q����z����O�5|6:B-�xB���4��_����ELI�oLug�3���P�m9�9K_m�^H��	%����
�C��(<y�O��Js�A H�YRAz H����>�@�!�C�L���$�X6W��l(�
�p3q'h&��;A#1�D�{��^�S�&�O�� $%�b@"Q�FNV�/}�������k��p�cT�S�~��l�>�_I�N�Wb�3b�!�]AlnV������K�<g�4��.�l<�=)��������dW��7��	�3����xt�R1��'���<�RH*��(�>���Q^�$��.)�(���C-���od��t�J����S�X�Si�HW�H�46~*����Hfy;�S��l8kx]I��x��T�� ���D������6�3������9�Z��Sk��.��GyQg��<$������+��h�gQ��%��4�@A������G���T7S�69A����&�a�C���r����P��?�%Xu-o�m��8����H�!l���(W-J��a<�b@�����u)J5oGf+��6r@��#��o�8������/y��l���:�m>L�(�}����Q���^�!����.
w����������4�Q�St���Iw�n�p'�N�I��"�	�H��;Ii�{t'��w��T��������w�m�+��S\}��L�9:����2�?��=�\�f�geU���#8���<�*y��V������v����(du3J��3je�.�aY���{�
^K-}�����]v`���`)�\�(��$�(E�K�� 9 ��*Bz�U�f��aX�-V�E7�������m4{w�fo���S4z�M����n��T
�xswu�����+I��DM*�VQ���Ur�Y�y8n8��*��|��T*�y5(��Z��Q���j=
����4�F�3����z*���7������q_�6�kx����y���1�c�W?K�a��4��G��.���)�G��;K����f�[F�4��~� �@\�����4���{� ��� ��'(�!r��<
B�i�����9<��s�z�@�%�^��]��}w:��	�����7�8��Z����5�D��R�B��;`&��O�������?���,�o���i(��(��~�������^ ��:D?��'���\�S���!W������y^�ix��j[��=|/d�yP�����SZ=��9t��
��]����Rp|�!��������h���kZ����c���k��%�U/U��e���|�7�����_<z���&��������gc�*/��q�x,��$��l��v��r#�%zJ��}�n5N�=�j~�J^�H4-���"�������S;OoN��]$*������v �
�����T?q���gs_�M��e-�(R8U�}Y�X~�X>���}3>��Q��x�X�d�����
��u}��T�
|.����8���|z���%~n>��}\��i`�CUf��#1|�
rP�`�JC��R����W����yhB`Fa��=�]�3�/����<T$6/��4���t�Z��D���^h�D6
�K�|�l��`��Q`�g���b��[��0����{�M���{��^:��x
�
s����ovE���vs��YW?6��L���t\r��K���;ET2��)b�����G$����*Y�F���L��}���l+�R���A�����z���p�\�G���9�����y\��O�(��E���
�+�X2/��t<����Yl��p� [��:�~z��e-���|�"������I�ib�/v�`���C��Tq��w�e��}��a�����Y�w�*"��k{byQ����
o~}���DkO��$�Q6����.a�x���(c�U����>FY����?����-!���lC�u����Z
j�I|*�������Jz��3���s�(F��H]���3���`���Jyb]��60Hx�RY����@��Ur��xwo�Ix�RZ���b���G����-����<�.�%�`��(
B�C���� W;?�v�1|\��c<�����x�q�<��>+�]P�3)s%
�����
Z`>f�b;�6E�a���K�v�������F���0�x&���
}{#Q������~����o���fOk�nkf����Rz��OC���)R8I�fA2�2FA����I:
r3��@�br3�y�'�(��jA��t%��c��x�v�[����j�m�-��n���������jw�Vm�M�����X���D�>m\I��D@k+����r�To�s��
^;h�
���K�d�L�����&t�P\7k$�0���`2I(�.Y������?�R��������1M��;�[�vJw�4��
k����i5��"��0c4�P���l�n�=<:u&<��495;�.�����s1��(�i��g��y7�$o0@B���8��	Vy�v��4�D����V�^y�����Z�\�_�zp���=��0n��{8�����@���]�jn-l`�����T�k�o�������Sy�
�����Y�[^�4��^8'%�=������&����U�\l������6�*�1V��5.��#"h;����,T��0�R�?�%���d�_�]���1�o���%�,�^�^�/���=������
�Ev�M"��%��E��N���
�Eg�w�,d��7�7�;zD�R�d�9��*���A�8���c�4q�Ns��bvb�;M���i"����N��D/w��e'v]V���~�W'>��Q����h����o�����s4�[E�{q��X���(�����+6��~��/��J[:Lpa����-�r9_������ROh!��*s]K�;��(S��s��4��q��������P�Z��k��U0:�u%
���\`.K^F�����ix-�q�(<��<sr���Vxky����5������{�"����ry�uo+<y�\�c�����2<�z:�WKJG}:�@�g@����-P��D�:�2�M��N�UoCnT@�r�2���(g�"\ �[�}���������o�S�[Vz-w�^�J��N�gY���)z,+��;Me��r�����U�4}���������O5#��KUD[&4����\�������z9�R��}Ro�\k�z��Xm �a����_.6r��OEO���M_���ZB{�vS�|�p��"�4��gjb��4L2���8�K��{�qk�+O�`�`�<�u�c,�dBw$�va76:�X�3����t,�id�����+3��S\����RT��~#��=��g�K9��}��D;�2�G@m3��c&��g��G�ZHxd 
f{���CkB�{�~s6�����}(�4f��6*�o������/�o�����~@������`��#'=+94?�Z�j��E���kB��C��� �������x��T:i(\$j�%�cn����4n���
�a�����"�1"�I����~
3L�5�0�A���"N_l�b��/��~����d���i\�)`��2�K�"G�m��U�g��{d�����y���"v�������o��:e�C���A]��8>c���q8�N���I����0WO��^��3��P�hx4\_>����E�[�^����B�4��zv������ f��D����S��I:�8Kpo;��N���]�����;����6��2!j�b	P��h��:5�TM�����h�'��-��P���D5w�Z���*�=�X����������^��P@���6��]!uA_[���5�����Q�����h>>VGj
gC��:�%��1lmc�X��k����1W�GEyy��/���o\���sw�qr@6��a��x��c��Qi���g���v|&C��lb~�+p�	b���r��u��A~q��5k_���9��{"_��������H�|���r�k6�1��o�����WO���t�o�Q�������������������4*K��o�o��
|K<Y�%�Y�F
�3��L���m�O����a���0Wi���W.��/���	�������py�!����T�������_X������������0\���=�B��!����*zb���v:�v��5��X�|}a�|y�z)����iT���KT��v,��n���S��W�56���D4�������n����zc)�<�$�9a�:eI5���I�9�^I�I�		�1���2�9�K�����L�0���3��LU-.�Cg
[�5{��o��	��Z��M��$`#|�)l�$L|�N?lk?p�l��
��I�� ��'pI�fwR�8����I�Y^�����9�V�b������X�����6��8�+7G������	���p8a���3�m�g�`G��q	u��u	�Y��R����v"����F�i��z��3�-0��*a�dd��Y���Bm�p&	�V���x�/���V����|y}O����TE���@��V�$�h}���Vw*o5���T�j�S��<����D�ES9qNI�����f3���h�S���X��XOA�L1�N��)�F�N��_�"��J�}�����/���/J���;z����*�n�+g�����:��vE5��"B�3>l�J��S�� Q�W-Q5���P��@�Mr�I�5O���7��h�U;M5?����r��BP�^�	W����+�j�<���IA!����.y��IA!6{��!�`@����-@�ba?g��
q?��0��!���t�nn��GD�d��Xd� ="AOd���
QOt�y;S�kDD"�������m�G#����-@�bQ?\��������a���D<"��-"b���	����NX�|���c������ y5���S��&Ww���d��N��LZ�	�V�)�;E�j2au�HXM���D8]5���$�SUw�T�d��N���LS�	�T�I�;A�j0Eu�HQM&��	����	�@zj*9���}�������
���\�%������l��]��- ��7v��E@*OG~������������|��TE�W<���������%��Fk�N��;h�mH��m�����#��H&n�G�R~dd���9D�����y\��]�I�A�6h�����7���?�nW���Z�~uN2h���45t����[���`� ��>�6��B2��5wu���1_`��j!>�$U�I,�r��4��E��,6�-�,��$�}ex�Ip�p)�W{f��f
j�,�Kap�i?U����O3�F
|�&k����
�`su5�����j6V�M��l��;AC5�L���%��O�(��/b��W���F\]A��
e?`�c4�9^��/%����i~)�k(�b���t����T������|�TS���_���
�V��a����JQ��q(u(��J���=yJw`
Co��V?��;(���@l�q�t����:����Q��3:�������S��m�u��\�~����	�mgu�/pn�=���Dw���vn�j.�[�F�L�f�+qL����L����NCl������@4�m�o�����B
�=��4�K���-�����,��(�*�a�27s�)r���=�����i�������L,�S0�C���`�,�d���&��(��|��6?�")�3�1.�*/FKI3���_�D��n������\��B6j6�w@���?8��|Y�1�l6�Zj'��*��$J2�a�	���U�����q[F}�%��k���S�����@qSq���y8;�K8mIn6~�8�-n)5�F9����.����������m��,�����'	z}eZQl����s��]��q9��C��VS����>�������	�S������P�_�g7{��j�P�K3�����x��H8]����7�����"���Ma���w��p��u(���m����Xo�+���C��<��.�M����j���������}q�y`���6�����Kp>?U�l���\9���c5��>��zvq���������u�<	�G#
[� ��G�#�g}9����9�3>I
������U��5���]�|����z�*�����
Zx�!�eqvA��1�3�$\�A k*����><���.R2�>����2�FPZ�xy���d�����0�bS���d\�	^�i���f�)d�s��j����j[������ pjp9%��Y������cv�G�&�/��/e��j_}�:����3k����A
����g�����g���U@��|5������A�V��p+����|����)��A�@*!p��5�)*c�B8���H���hg$����:Jh_�b�T����S�)X
�YT�
~;�}2�,_����Y>��P�1h�f+����8��DX��?��?��H��.����v}�h~�V1�Q~���M��%���Lk���1>��7��q*�T���4�\J%��nw��="�r7�g�G��Q�|DO�!�B��18�Y�UI����2�k��`���)��aU[b�m�L��������:FS���}��=Q�w��+��/I�7��9�����O���.>1��v�N�d��I*�s���s��N�,��((�����6:�N�-��t����������/,K�^b'g�R>c��������1������;7�_�3.��)^P�zz����$~2C��t�w�qw�l�a'�������g�1�uQ�W�?/��c\b+���@�������8P�0#���c��J�o��z��1l���;],�Y8��~����BH�c��TeG�����_|q�]��N�f�a�*�@��vO��= �p'��8N`�G�#����m=9(�8V�H����
���z��9lEa�I�E��k�Y�^�,�=�ie
(X|�]���c4�]������@W}��L��M���"�K��I@�MF`|���	�����N�,D&w��d!.��%Q�� *Y�I�$1�|D"�%\�/v�>|}E��i���}����/���n6!���W��P,��=�j%���m��V�l��7�>/V^`�I~�[���i6��Y$H��Z�Ko��J����O�"�[7����m�$�{���y=�j[j�f_
DS	{TW���;t���t�x�P��4�@�A��Qm^o�d�7G}z�7��������UE
yj��66NI�q�FA6<�'�r���c�K�</�y���cQ�m����@��N�����h�`E��3%����)�n�sJ�y^;h�k����O�HS����S��
��	�-��\>�/X7��E=��)�mF/Y�����\x7=��~E#��o*�o�����L��_8��Eo:�-���)
�zJ�7����:~�v�5+nc�;Hn�gj�#Y0������=��	f�0��+w��E�����b�����	���g(qS�qS�Z��9�5n��m"|U��q���VH
�4����[�X���%�,c4Gq�T�cB��}W�F����G	�� ~�������v��s����k3������L���d{��g�K)����f�����
l����.�S/����TK@�K�/6��3�D���I�T18R��*�
"�I1���hac��9��x�p����7����(�Z�y�s��8���>�K���k�y�T7����G(V��{����$JP�W�KR���d�C\$�
����Y�-�
�qg	;�>��
��%��iUs&�a�Ip
���<=1�e~{OF��DV��=J�!����i���.������G%
�K�SC�\���p�BS6WD6'������hO�O`3$�9!�q6�7���M~�[7����x�8&����x��	a��`lC�L����F_}Un��y�����~%��gZ���b�A	���wI�0���6>��W���)��%>D��k��6�D�L�(�b���U>���k�dVh6�>�$hR�t�X�#!����;iRV��\n7(�=(W��7�7����Q��R���]�?����w����%rf��H����/H�
)�}��6Y��W�1�����?���:h���d�'�m)K�����{����W��mDr��4�a����9Q���x7��+��K^$?��6�p-��d�H��l�� �0J=�Q�w
Q�����i�7�(�q�
�me���q����c\lP����MlyeO��.���h����������7b@~A��1^�qv=rH�a��{��l�uH���
�~���5H���A�6p*b/�
�8t<#M�\7C���x��'������N��@<9N$�exY���Q���^�6i��4�lc�p�@4
��S����7�����*��-��*�y��������e������/X?���J������0I ������Y��Yf��F
���D���@Y�Z
V(k�
���d!��"�7��B&E�h�k��#�y#�0B�D��f���$�/�8�&|e���s&�#�������:�����QT&J�����,��&�U��x��tH��sE�W��1��I�m�����0���rgl���V�UW;��
��vH5P]��j6�������D��N(u'���;����h7�U�8�������&��qf��R@@��w�+��~429zx@K�a�k)(�(L� �����Yq&�(,rH�/��W|J�I���s���C��?��U������t�Q����a����)��a���f�����wed��>]�@'}|V��H5@����kG��1��x���^H6c��R��$���6����&�&-��3�����D�3%>�V-P�b�*�@�(�����P��(K�������#������oC�m�|r�hA���;E�1�zF�%y5�B��P���l�VV�i[m�8�Q���
_�7�� ^�����r ���L]��Z�MXn ������m��	%��0�w(	�$��L�������+M��;�O��Hw�4��c]�����O���Y��3�0��������Yd��ey�B��z��z�@���3�m���x��[��m�L��3� ���Lb�A����y���F���u&�5�;K������;��G�qv����g
�k=z'Cw�����	�����.�$y���_d����O?���w./��?���/=7'��FT	���\�s����]��������=����
��(���6��++�rA(�E�e�qR��F�wy���+e�a(�U�Dk.	jx�����V�S.����KEGk�A��~����g+� $�P�]Y����UHB���0~��xg/(z��|�������������Do��/�����=�ry�X��x�{�[B��Q��_E�3���Ts1�p����i�;�H��8E	08N1?Jq��X��S�Q��P��F(v�'�T�K�w������;�������fdbe\�N2.�0*q'�X���c�#W{Db~<�j�G��F\�������=1?q�G"��!��8��(�-A������F���E���e�U\�B5�Pz!��A�7�4O�gZ��^D��T��Y�YF ��0\?��o?�%ee����L)X\z���\�������5U����l�����8�w�)u\��v,�Nc�w����������ou|����aNqc�S��Cs�9��x7v�#yA���7�K�@�
>��M��4���K�������kk)�=��3���9n�P�C�h�������%���~�����|�Z�9�:�_7RBI����<�|�����"�������uTw2G�����nj�I]='5��������X�����Zk���UiY�Z+����~�;	E8�b`��G1utF�]R�e�<7�d=��r"�6J��$�����%��S+
ej���y����P�D�l�F��<��'Z��M>8Bx�O8�Z��8#�����po@�W��w�po�	W=+g{~��0]*�>��� ��3���IK�IT^�\u���b\�1�{�qY���M�EW3.����fT4]��h:"���t<t���f���

�Bw�Xh<�d��B{Q�=��IT�2Z�Y���(b���Sb/+~����}�w6�j4	O�N�E{�8��>`�,�:h��w�+� �����;� *��ji����[>-^v���P�J0���^�b�t����y-	���9�2�V�/cjE�h�-���v�a���Wq��0��E���5t��]g�u�������[i��4���.�����n�7�w�$���-\b&�e3�?:��5���7t���BV�o���;���4���Zov{���q�9q���w-d�z
��b34�=-�'��]�~�{]E1f�C\���z��u)�*�m�@��/�&5�$)+O"5�"uBx�sMR��$N�Z�N�N�RZ��hd�(�SJ�a3�R�f����a�D�<CS0�ax�c0��[��]��v����#����hQj��].�&v~c���~GU�7��B��H�a����n5��X�~���}!K��Ux���0��/��)�N�'yX���MV����x�c�a������OI-�|
e����T�_2m�:!��h���B���_".�r��h��^�It������w����x�w��0��Ke�7B Fq�f!�hu������3����>��XL� ����\�O�v>K�����U�%,�ut&�5��j�������{'�r,��Ln�`�X@�W�F���B��k��9��r��L{n���"�*Q
�����Fvc_��1Hn�k��������W�~��_���~���|�������
�A��kL����^���fD�9S�SJ���5��,�v�(�t����a���qG���NEa��Z�`�Y|W����.�bLZ���=���R�e�q����r}���C������,RL����r�iQd")7[��f��UT��f��Y}�|�i>"���"X��1�vl0_3M����_>��������o������?��V4����-������d��th�}��-��E����o�"��wH�����2�9~�EU�a���o���;H��C��
mWA�{��y��T�����d�/,�`8���8D�j`��}iT�Z���������������#�rY��5*�s�><���B{J�
�*���(A�1����@�gBA��!�L���q;�hjB�\8g�#5���.�s�055z!����G
M������Z	@,� �����<��T:���-��X7��V��a|A�����B�5K���R��f�.��_
����m��&�Q|L
��+����/����\�W/�,�^`1V\��b8���A�lHir�4.��l���EUqq#��'��(����B�L��P�*)?� 3����.��=�-{�qU��N��:�
y�5Ou
��@�D���[�y�/|�8���'���N��g�E��o�o	PTi�`l#�L��.����F���t�P�yi�D&%Q�)\=�s7i�GTE�}�	@iTGf
W�F��>��q���@(���@�(�>���9}t����S{r�b������������o�������pI�������*��O$���$!L*�2����n�������V(������G�`����iv�A����X��a3��"}|tv1M�c�
C�7���x"	3
`3��*�T��M6v��J�S�1dCI�]R�m���a������c*�������l�Y�)\��S��e���L��0���L�:�>�\����t
������TT�����L�f������������M��~�$�R��#��a�_|e�f�������{[�ym ��"��akO��`��������5	�����c��������F�D+j.��N`EX/�K��WU��e�l�`�@a���_�I���dP<&>��3����sR���}�
�����6.��m_
�S��{��(���L@�d��'�jc|��(�#��G�i!���!�P>p���={|L�mT�NYI�w2�J73'?���C�F�&.�����������s9(�$�f�^z��������k9�:,*������?����*oi|�b.����n�^J�q�S�R���Lj�%�#�����#�7�����{��?]��a�~�2��p~g�,��4\fr�3El��'[y�h�)��y�1�2������CLd�l@cr�a���$1�j�o4�-%�R������p���m7���p�q'h5f��;E�1�b�)Z���r���z*��T�z��?�iT�8�k��
�0��L��Hr�O�S�V��+�Yl-z�k�;�����>"���L[KXa^�����Z�������,�9J���b���N��w��o�����~�m����o����"���X��<%iR�9����w�?���>:����~w8e;���>u�����������������gs��p��*�y	Nt�i~D�#�Q�g\����Q�&v������;wBY�2�?�����s���(�>�+���� l��8�����G�4���4"���jh�������I�F�x��LB�LAk�l�1��4�9v�h��|���8��N�c�C�3�/C)��.%@g[w����������8X^�6N���)l�[����.A&��_y5){���f��	����3��b���W������VT��Us���'���Nb q�y�
�8c��8��J�����I��<��r#E��#(�cl�����(/����"ugoe%�84�U����
�4
��w~t"^��S�s���y��Iv����>����k���o~�=:���_�����
C��������X��y
}� ���Ff$K\��:L���P�=B^41���mc<�u�
�$�}B41�Q���'Hy�V{	�&����(�!�1������P������C��AS�������z�B��D:�l�H�dA�|b`��d�����Zni����u0���S�`E%�~$��~�����\!������C�Z���������6�=�Di��[y��>� �!OwM�����������=�����d�vA�� ��#j)p��/M'}*��$�FY�e~��Q�A�ZdA��y�ds���Z+&�vC�4�fkhj�'���4��+��!7�x�%3��Dyi����9Vts��Q��
���(X
��ep�����.lPM�_qIl$�VQ�E��M\~�f���j�A�L�j1n
dK�����|E�<�I�/�������I
"��N�(�I��&�}zc�)�ym|�qu*�����p�A���Z~����4r��l4V���C����}�,X��a���"�����FZj�x�yV�EE����Q����n�P�Pz���L�D����`�g��x����ej]q:O|?��dg�`���?��^
{{���L���K[Avp��
�^�+�N�<�]G�h�l��2�;����}�U��:#�F��2���'Q����bw�>������W�5���b	�Q�A
���S�g���O����:�v��	����_��ff/K>�����-����~#?�i�7d�I�!�x���8x�'7���9@cj/�Z1��6��$���jh����-m���c�M��s�6�[v��b�"�n�*1l��$���0���^�Y�-��Vg�2?�����eV}]��{�g������#5�S�"����L^E�h�PF����M�ZW����MP��A���7�d�bNVf�����l5-�����a6�m����{w���$����H\��q.��-�3�!S�Rz!�
���Ivgv����[_*y��l���������2�z�-b�v|I�����i�tiZ���\m����B���E��d'�����U������B�2f�^l\3
a� w�6w������*>||M���$�s[�bd�d!�����MqJ�]��v�|i��0{r�XO�jh;[+�bR��h�����	7Bz��:>��Y��$�6�v7�SHe�&�'2�
�7�m�Mc�%A+�~o
������S�����-�Q�LH�F�=	�i��f�<�e���& #�e�Q��������K�l��A�
H`MCml ��3�"9��v��`�-zz���U�-���� �R d]�������.;�{�a��P�`�`�o�|��O��k�F=Q�u����mKs�gem.��Rs�C�D����UYQ�U����c@(�r��������,�-�}o��d����'
�1Hln�H V�� \��9��Q������
��<�V���S��X�V��sS�KB�p��qv���0T�NT�������;0�����H������O�=�P��5,�Ed���7�i��o��=�,��5��a�F�l����*�}zO6�M�km�T�y	���l����Y�;����7��������I�����Z>��lT�f&?�2���$�['�����ec��N�wP����8�zgQ�����SZ�����6:���w�q�s��1V2L�����a�5�*����Q_��
9u���B{�~�kP��7�W�~��S��K����g�|�XO�����b�����^��/����?���?��>n�����w�������
0�#*��j�[�������i������~�\����d�@U�`�d^�+?����3u��V�F�8t�(�&���?�F�)����Q��Ju�����3��o���<�R�b1�;!���ueuC�����E�������5tu�y���b���	 �Alg����z��`9��uI2��b���
��d/(r&
���xM��A���	�����X=����������������.�/��4�]w,�ZRnNG�A?�������+.�7����B�DK������;7�w�������4n�����}O���f���������+`�����N��h�Zz~?K}-x7������B*m.�k
U�+��-OD}��u�O��6\����d���c�ep�8e�'�`�������mn�B�$���-��}��?��^��z�z(�Z�������\#�F�s�����/��m!������T��opG)?�n�k��U\��6�?�������
1���CF��n[!�5��2�P�������������_������������o��q|3��ZQ����	B�B�k��vY�)��hoXc�0�$�44�6���K���������o��o������R�����
�
�w���f)%���Fg�/�V�J_j"f+�U[�����������zPK���/
����M��ed��/�J�0�dz���yi�m�Um/?~�g�������
�h���$�V�!��W������c��%���a��@��q[B����}������o�`DY�#��Dh@d��P#������Y�uf���Lh "f+�U[������|
c�4�A5���3Q�i��� Vm7g�Yl5D���xJ�����������b�b��]t���'�E�a[@��(��C���p#\�V6�����aj���P^_#��a
s�w�����Dk�/m�BdR�]�@�1m\�hc8�����p�qu#��8����Q���2�c�;A�1a���e�1�t����"������W��,v��"d����?���1/�2�gT�3|�6{�u�
��������i��3����RIo&g�������IY�xk��0�E
���h�������:D��r�����FW�$�������V��(k��*.��V^��]�h�Q����)�(/�n�kTG\���_���K����
��C��*MJW7P��$a�F�t�	�VB��"MHw�i#<�����}�B[���/]���D�X]���g\�qKh�i�W1��=7
|��
A4�Q��a��v{�Q�yn��xD�Ro&�Wx7��+#4�o�^�*W	"���?��UN�X�Il��v��q�_�u�t�y	x�Y��x�	B9���V8���T
����3\���v�����t����Y������z��W%]�da��(x���
F5�F��M1�m�[�s�����W]AiM��f5\�!��6��q��G��H(����������U���0�(R8��v���h�����.v��Y�w���iB���yJ>��Z������Y Fx9����
���zj��2��km[���-�NU�M�b���j�^�Fc��X� ~}o��f�w�_P��IB�g�a�
��9��8�b�X��jM,���������oI�n�<D�h�+de����f�b��~c�d�P7�������N
d�Y�
�?|�����>��Ta����� 3) � H�/h�TVr;vU����SQ�B�*��4���������w��{-;=���T�������ZR���I�m�\%E�:9��m(���m��B
JpWa(;�����D�Q�-q�G��������r�G�YB��(�:V�
Lo)(�
�`(2�\�@d>��a�|r�����j� ���@���;A�1|n��\[������c:+E�72�g� v�q���@�����G(&�'��5�>��
��Q66��U�!i����f���Q���;��wRwB'��������s���4V�4��y�$1�D@c\���4Q�d�(
cT���<�2m6�a�P��Y8N�(Uf�l�:�%����ia�#���
�i-�Kp�&|4	���V�J=��@9�9?��Vvp�S[��gl	g��\���[�s��:�i��=�gx�3��3�q��pR&�H@�Y.42Y�K�~\��\��d!&��1�BDr�#��x���#���$��E����.����z��Bt���<��^
L��N|1���������b@�:)������vki���Pw�����i�;]}�����d��Yys<��S-�v��V�u�
�#��=��I?���Z�]���ZJ=�������EnJ�3���y�,�T�x�r��?�oT\"�k�-0t����]��R�%1MJ���\��E�rA|&�]��6N�M�6�z��������W�V��Zx� ��n���+�ohu����M�t��%.cQ��T�;��O9�"��Px�H��	5{�s^�D���b�|��@������IB	(�Y��L���cK(ce�G�����r~���{��Eef	%8[,���������\@z!
K��d>����|4r����X�j�"�����D������G!w�(d%�3��9~����h(=������XZ�BI���A�N&���}�/�j��4\]��a	/��`�5��	�FE&L�y�0�n��(<xY�C=Zz�`���=����Y�%�m��E�QlR��;I�����	�����N��,4%w��d�!��->�����z>e�����YC)zz*�W��������]�R �1|F�����s	�v�.)��K����k����a!+&��V�}��$��TO�����oQ��-�*%���)���`C���p��0�v�����K��q5{���8����b������%2���������1��J.>G�$}#ThE�!)+A4
�+��+����|3�@?�����Y6^~;��a�����N��c~�4���y�1���kS�Q�n�xs �w �!D5�<��M��r�������)nC�#���f���b�S�G�mTV����������>�Do���wFy��K^V�8�.\��:<�������sG//��~�����C����
~8(���w�o����l����l�@.q+Z�Q����
���w{Co%ONq�<������7���@������Y�v�2x��d�$Dl�$����7}�k���)�����k�����$��w�m�����#���Y]���nB���
V.��� ������,E1e';���{6t�^��a�!�����V9�s�z�6PR<�'5�I������8��K}�6P\�'��
� ��lX�
�K�m����P�PK��N�����y�� �a����$��aI�GB�)�?S��>�(���E�
����}��L�A�`R�"�_����b���8X����l8f�|�rP�A���?�H����l��)�C\��q%��i.B���l|tu������FG����"6������x\tu�����N���v���En_��q���/e���S�����c��eK��w�J
,���	9�!@��A�7�q�����j&���/������3�8JW���k�5��	�J~�s{b�=�|��]�Xt����
���d��1������E��X�t�k{�F���x��-6��1?W������UhF�G�C�A�l0�����G"��oz-%���b���E�~��-x�7U!
$��X��W�J�,J�7e��3Ej#����Oo:f��B�d�SyL�����Z�
5"Qk��[��b}�O�	
��#)�t��"�1�eF��?�%��u�	��=O���=6c������a�"a���1��I������+>���el�Bjfk�a���!�
�
"p��aS�g%Y�q�-�\	
���;(�i}�_G����?���w!5��
��a���3��k������C$��V��Z��?�q�ztY����������E&��#���1�WE"v8e�B�,��e~�����r��E���_s��)`��������F�DAo{c2������x�5wK���!����#�^���7�����}a�N��c�
Oz���R����-��}��|�o��������l�S�_%N�R,�F����5iZW��&
�N`X�f�]�f�M�"Oa�%*_�c_����R"4��=F�������M�����x����]m7��4,I ��Y�w�>�?[�\{
���r������Y�J�,�y����hq�+y�OS����������QJ�l�**��{bP���������l����\�UF$�>No���z��i�Kf��l�|d������?����uvQ���#�����~� f�{Y�Y�2E�v1�U�:i�I=��
u�W3��Xi�+�����0M��O�������c�~�w�U�u>yD#��Jkj�fG>=�vx���f����D��*3xb{K�Z�"����8��u%������v�m�h��^q�y\l�O��t���@��'IE�$~�\��:�
��H��TV��U^7�X���d���Fi<n�y�����c;� �cr�)���T'��;@w
��2���ue�<=U���#���(�}f��\��:D�����������)��8;�-WQ�F�CpZA��,����8���_����q��qM�S�ss��o�bc�4��Z������������=xS�9.�q��y��8e[{-A�C
�����y�������<��.���Kk��}�"w[�(�����R��E�VP�h`W�1��4%���������������I��J�A~�\3��su�i.���Z�B�i����.�aoH$��C7�
�`}��I�u����P�Z@o%n�k��W���wA2��%��=F�0������)�h�j��f�l"d�#���Pz�P��Vq��-���Y����pJ�ae�P����]�j+�b���!��������/5���S����Qw�~�|/�N����C�)�P�=�;Ej��t��?��������������s�S��V�MW����k6�T��)=��U��=���5t��XEe��SQ�Z��T�A���2(�L�T��s�N��\������$���U�����'1������$��*4
����S�T��Y��?��/����O��._%� ���p�L�rK��St��:>��.)N�}T$��a��~��f_+� ���f����lN�0q(��(�`l�b6�:OQ;q]�8����T��%��5��R���Mg�btUR�%�
�]wx����Z�����i���5��������`|o�S�b[��c|��%�����\#�4�m�!�b�xo�K?v�>�J'�D���"�z"y7��s�g
�n�}�{����ohG ��^_�W��6���q�������@��7�����oT���{lyZ���������:=�yDww_����x���=%�����U	�t�tO�q��P�n>�����^_>�iu���]��;q�_�x!�2F8�����3��<�d�W��^6��8�U��Ai�=��1����Lks�����"��h1?��������u8;_���/�r�I��F�L:S�s��^6O��O1�9��JN���@�/������6?�;��UT�v|�*K���:/IY��":���[�WM�Oq��,���^W����X"��R ��a�
�����^�����\�j_���\$|1�����Wb�����y�;�g��kw:�����t^m��G"�r~V�kkee9�!�0M�l:O�Px=��7'����-��B����� R�9��v�U� ���������
nT�m�Iy(�8b[x��g��=��KI������pYp����\#���~�2/�����[H������##���
6�����x�Ug���d';e��(0_���/<��zTx����.]M.
3y{�|t�qz�k��5u��AD�����B�eN_���������9)��}%��}8�y���$�4�v�����x�����l?Z�Z�}V�f�)��e�C-Di�9��m����*:�����l�=��9$������zu~fcMQ�3�r#��L!f��kKw���k����K��d<�a�
"�)��E�:�s=5����Z�)�kQ<4��5(��5��}�:����=ND�t�
`� ���u��������:TO�q�j�^I%�B'Fv��.(M��
2���2O�|����Pj�f2�<�B4�KBy�Z�9F�XZ8> ���]T��������KAD@���8�o��t��#�����D�x� ���:O�����>������O���7��_��v_q;��Y�Z�1NibC�x��2G<�<',��;�Oe������}/.�����~���rA�\�0�1��A#��,�K8�$+,�*7
C�z����B�d�����_I��b��M��F�K����d�l�y�-\F���������bP����yi�c^��S���?�������u�ZeZmdO���J}*���r#�/�������4��vC%^�0������r��z]#w��4����X�K1t��]9���l���*�}����f�Y1�����7�������
o#a�;p���v���,j��+����[���%�q;��pZ-��"���*L$J7���q��F�"��b�K�5��s�m#��
��>\H�*8����/2 �
$�����p����|�E��_�S,����3n��4R&�O�+{&~03P���2P�������<xc�?X��%r���^��'� ~q��^�:�S����	�(����
=nzn{��5�M�
���3������P���<%<���o��s�O�SGa9�`�`6�����Jp�
~��Z\1��7�he�P��g�Hi�2��e��G-������^�U����VL�������l��ToN\V�A��b���?���~���Sq5�8d,�yd#a��r$�4�)fx���M�qG���w��V�j���������O|^K�s����g�n����je�������V������b���_s�o��5�
z���2��VF��"����l"�����.��a`�����iZ��F:��_�O�"X'��X�&>��>�2n
>����{��
���5d���[v�rr[@[��`�mT��FY����J�C\�&�b��*#��5���*���w,"N��
Y���j	�b�{;r]���b�"��E�����V�]�����d�d0�w@~+V�^uV����R@��1�2������7�r6��z-��.�o����������\FmK0�w@~?�)�Ea����4�dP��Q����������B�����<!���(�������[T��E��S&2��Eq�is�3z��[;e����#�E���0J�sE
��s�/�i�����K���������IPQdX�P��3�.�)�QL+����VZX����)��.~v6MO���V��[��7��%#P�	��t;�j��G�������
y."3���������k+TE$R�<�g� 7ru�=��S��DS��,ky�I2f+�����r��������{�M��	�Z�-��k&�B�3q��JO&�\t�[CNo6�6�3i���(M~��C���P��\ �n���~m���a�K�vq+��A���cZ���IE����e��fj^S�n���V[�2t�6S[�j���~k��:r��5Z�b^���${�=j2�E�-��I���������B��&Hh� ���,NM��Bsm��+�I�S����I��V(���0O����,N���YD=��[7��5�KE{j-����G�L����]>b:�=����nzv{>���E;9��\t�QK	�*�s^_^C]��f~�R7C���#6�W��Y���z	]��[Js	�*`�p5
���z�@�I�!�����4��<��3�ZRAl�^6��c�=c�I�:r�v`��_��3��
Q.%�}8��U�yzb~Hr-
5	���e���oq2$��M�:�Q���X�p~
���&���_�����N�m4}:�[����(�w�"�����@M��o����a�s��tP�0�[t�}U�
��Y'nO'�5
�����Bb���){s�x��%����*��.u�V��f@�l1�-��R��>E�F��NGm3�r�_L1�����[2���^f�@4Poi[n�����d%�lDS5����$�l�X������pL� 5�L������i�l)�q������^m�6�j��v�C6�l����z=�x�d�L��t�x��Y���m���n�U�� la�6*��z[���	���4n���G�5�*��b�y����9��,-x�J�)��T����S_d[���`q�����q�V�r��
j{���<������vDL�o�S.D�6�k�������5�p����P��N���~���w���
�e$m���W�������!ly�d`;p��^�f�F�&4���IP����i����Cq;�a�����]���-���z��D�0c4��:_����]����I+H$��aR7
��v�n�m7u1���}�A�0������=�J(X���.x����l��#��/��Q��x9YKR�J*j!v��5����*6�
P��j����DOD�[E�Q�m������E���k06���B�k,�	�z{�����?��t���e��/0������fW����Z��@���K��7���E����\�
�����e��G�'�5��}pT��[��0�4�V��0�TJ,l�ce��<�d��p��������=`�-��B�Al.��+���[F�7��![L���L��x�b��@��qFI��,duC~=f����7b��9{P�t\�MX��+iv-���n
�������'�
G��/�u��P��!L^��5;Q7�{5tl�+��^-���t8����C�lmx����=�q�;$��T���!�������������@���,�=��Bc��g�ZX�A/��]WR��mm���@�T��-a'G��B+�q��]O/�P#[@��&���7������������%oKc�L
��|��$Ab��Tn>�����)��}���l�g�|��U���,��������1b�����<�svI`����nJ�&��Q�Q�Wz�{kX�,:�(�+�'�H ��� ����tS%���m�;[	�@l�^�?*)�A^����w\�c�$W��0�E�A3�?�lA;C�dFA�0r!7 �Z�|{�q��v4l���ie��>0h�m����6L��&^t����v������)F����U}b;���8l�1o�m9co �����A&����������moe;d��H�<j�OI0! ���7�������%_l���l��,��s	�2:&E�t#~��R�S>���W���o�m�JfC�p������dNh"0>���gM72�b�`{v���@etp^��dNi�0}p^��`a �����d�	������Mx���!4�B����M��P����������	��CB^w�&��������OG�>���"`�R@[�}�a}���_J@'N�����A��\Rz!rPGxv�����J�t8��X�B��/��%*7U����c��&i�2|����8"?��V���0 s��6�E|m����;�Z0�.�j�]p�W��	��kFW\�����z��������]p������+F�\�u����*��5W{
��
�H�W�������f��	f�
��������w'��78��N0�op���b����]�9s3��3����	��M�������w'��78����f�
�������f��	f�
��������w'��78��N0�opv��b�����]��}C3������y}w�y}����������������4]�����o��N���gN��8�������y�?u��F+����+Km4-������n���	
(t�Q?��tt������T��bb.5���bR���O�������x�t�������n�m8���������m��
n5���:z�Mm5���!��U7�����z��e��g[�;/��7_���N��G��{�I���&�X����r
������A��x�(�5&��
�@IDu5y=��)��Ms>�5��,�
s>�-
kA���f�����o$0�F\LaDK�O{������o��=���Z�$~�v�R�`��i�6OO��f��E��p)���������j3U�'��K���j��������8��*C���������������S�y9��S2\=m�Q��"E�����MB�	/&7�m4|��.�4q��Z���Nj���lQU��/��4���z�0Bk����kG�k���6^N����7���V4�m����
���Z�]OnZ�h�N��gS�7�O�L3�
����hq2�3-������� �$5UM�I�M��RR�.'J(Q��Z���5��S?�����y���dsvmsd��eo�t�1�p���`z_����i3�&�/��eo��]N������;�B����+#b9�Y�i�
b������tA���O�1�2a��\��u5�U�kB����NeR������3�������0���~�\On�)���zb;���b���&Y����d����l[m��,)��6�����0sQ���������8�^Y���������b��\=�RM�k���+�E�}U��}%$��@�)���U"<��_��!��5Gs!	���������\��`���G��8�&q��������0�r"fK�||>[)@�L�p����Y����
<h= ���y%����j%�WCJ� ��w������f�(�����*������}�Pn��|xP#��j�,U����$����������m�������`����<����p���f�/��[���V�c*�^u*�xg�TQ��i`�m����Qo��
p{�Z.����W�����v��o�����0�%�/�
��(.��i�F"j:=
l�W2^��q��#
������j�����[D�d��+��
F���il�Z�@�ZZ�-�W�V��Qk��|�%�Z��f��P�j�zX�B�z�Kw����r�4y�k,[Ov�T�-����?Q�j�<W	�������������������m��[�.�<��M����Q�o?����Lh�B_�:(���K��Y�t�������������4��dsPv.M=��[D���+��l�Z�m_������4�Lt��`��'?e;6�r���X�
����-n^���t��!���^�~�)��������E<D�)J�G|�O6Fr�q�������������AcN��;�2jT����0	��4j�;RAc��a[�{m�I�-9u�w���d��S����n��]�������[��VFg����������u����J�"�-�]�����Un���'*Zoy�z�����Lh7
���+���[�F[
��u�]�����]�`B6^^����Oq���������]������b�!q_ :���o��������m+>]���w�����3��/'������ku����Z��������_>��.������_��Z�+{>�g��/����Ms���c�{�1�v��O�2.�J���xL���e.��<r
��O��x{�%�&~e|n��c��������������n,'�&�������4��v����K^VBh��c�������!���(e���^��d�����%.�����?��".���]����Fmx,����������g(j%�V�F������c��6�w�tx,�=����������/.�/.Gd�����=��=��:�*��{VTDO������\����-�o+�[([���n��������H#h>&f�k�w�S�%����IV�Y���XKe���9������(��Y������5Z�����r����/�Aa���UF�����?	~�O���|*�c���9�7�����3�c��m�)��=�&�&��x=R�|_����W�{���~x��X
���1�Bqoy�����������J��e&2n�K�����j��R�I��]��w�_�<�����$g�#���j~Z����)���_�?�i^��f�������q�(��h��?�3�q�Z�����;��FZ������S�-�~�K�-��,,�Y�M��Lxn��<A��A�v��y��S�X���1�L@-E����IU������N���ft��~P?O�N���#t
P�0oL_��V�����gWc�tB5d�X�F�q��t[��������[&So���f��!���F�5�C�aC��)*cG�J�1Ey�dg�LR j(-���J��Q��|6�g;��Xj��+i~X_�����d�}���p4�\�
[4��dt�4��>C�/;�eG�����/����=��7���N�����Ep��w�4x�C_�Z�1��Ug����%���}x���
���]���|��~�F�p1��wtV��6\mM���`��s�l�SQVr��{����}s��j���)���4�&��r9y�r�����o�~�r��@_,hDmM��1dO�F�)�����x�2W���K�n��w,8�fj�RAsj+b[�!k�7��}���W��vO�NR�>
����	DG�Z�-��s`�y?��y�q��:��oH*g���6��o���z	K�����1�t-�4�f�h�F$j/M�jp�f��������_��b6����Z��a�r��Ez?<.���t��L j5-����2���s���E8��e�?+��)�8����M���e5��5�45�&N��]nX��j9W��h�z���S�aY�l_�?(`�{�����v-4��vu�a��]%���Z7Em��RVq-��p�v���m��C��6�����W����mc;�m}�F�w�f��E��_��Q���	���JYSxW�G��2n��-vx�k��B$haM�j�e�"N�P.c���[|�Yt,y�N�vm����@��h���|���o�Vo1�tz~��#a���\��Shc_=K�D���09��^�l|M�t�G�!b_�-?�n������H����SQ�Z6<���ZS��q���]�#k�v�������`��|p�y���
}��������s�qe����X��ikb]��bv�������68c������L<t������;����"b�R~�����l�d���Oo�U�Iy��t���~g��x��H�v~8�(�����Y�G�'W3�����	�N�U��
�+��.0qH�����iR��e�.
S}1��,��vx������U���6oCF����Q�}u	�u#fm���C���ky;Y�O��n�9J/~"�{�X�9{��I�|�wYL��V�<�v��Zs0�kQ���%�o�/�/7��E(f��K����'Y�������:.\9�J�wQ�Nn�?��?$�I�TF�;��Q��Y�A�E����{�}X<f�I�{7��6�:�G���c��������q�1�KL��;�w�-�<f;b�tgw*xa���q���(K�({'o�A?�J�wQI�����=`�U3�}��1���wr�+��L��{(t?+(�M^V�����c�o��:f��/�5��ya���S�<�u��:za)k�,�3��1.����v��N����4*��B
s�
�@�-/���� �H���<���XlH9�.�5�.�~B�-��\�o�M��r����g-��8?�f�uSwRAK�kb[u;���y���am��]E�^��T3����pm5�*�m�*�������l��\"��DS�
����M$"��D,�X���>7a�}�!m���z��(M�-X,�J����!��Y��9�b�T�7g�:��k��O�8����/�6��K�,���E����|�_kM��{�[�U^E��e�����U����I����'a@���E���u8e���g��m����W���}�k�]�������8������R�n��^�:	�5�
��;���wy&�Z�s,�%�.�"a+��b����\&�fW���)c]�;����H���?q��f^�b�>8l�������b[%7_aVNv_(V����G��.�mf�������������!�z�(� �6/���4��VD���c��i�[|���m��r,|Ul+�d��)q���->^��uw���IF-<�F�������Rj;�Rs^�Z;��&6�*��3�x^\5���R�e����&���j�</=��t9�a�x
R4���Q��"�d2l��m>���W�Y�1���'R�}T��;��]y�l������3���d*��B�)j��g���i��������}pw�B��@�Z6��5�r�N�1n�P[�el������n���t���=��
��si���.p��$�6*vC���%d��s�b����������.wYb�"#5�s��e��\�2=c����W@n�:��%xT6e}+-��_�z����H�m	5h��s���K�e|���{�W��I�OcYHO������B�d�V�F�����L#G�����I����"����(o���]��J<��i�nz��E?�gdy3_[�~�]|�^^s}&�*Nkjc�.��9������iK�n�
�j�����Q�G���m�
����:��K����r�,���.~xP���i���x+G1CxJ��x�HY��L� [���W�������(=���*w��
��r�$�,}s��������"��b�-��,U���P[`0�����d]Zp�'^Z�g�[�B�*�g�O�s�����.�����B��n���i�U����
�1
�k�m0���;�6��-���8on��/z�F��/�@c4���a�h�q�����j����0F���^�w��b2�49������S�~�qtc�,:���yQ��������D�z�j���F`4�v��������!��)t���K��~��I���������F�u�4�������b{c�D}a$�'�1l�I(h-`3��F`���~��u�G��7gA���xOu��r���(`	~c�h��SY������Iy�Nb�)OviT�y�K0�^�	1GqN���ib�Y������|tVH^U�>[�4e�
\�AFQ��������cn|��U?OoB�`C��V?�"�:X�`,�T?���ak������=�B���_
i�!C6x�*FX71O������5�+����Q��<d��	K���~�������l&��|�;���Qu��|tm�Y3�����ZH�P�|����������������0�Q���Q|�&�~��2��e"~�_Bq����\*��'���^�����^��y���L~����23���.��f�J� 3
J�SH��Fw[�D��f��j��(�Mx���������{~�e����#L�`eye���X�0�E�����#W�{�x�&fI�`+D����\����Z�z�wA���*.x�c�T��\_�F��#�9Zfj���09����]�Pw�|[���h`C�x��4�m���3"���H�jO,���'���t}���C�>"��n�i�Kw.-0��+����e�|��o	���n�,c�d���� �fB�&�7� �7�n�p�i�5�K�W����F�{8<����vO�����`a����I�
�
�p��I�_����B:���C�����SOy�r�lW����d���M�?��z/��k'xeV���#�nU�b:��r�_F��#^yhS�l������B��r���V�Z��y?{,�o
���G��6yN��s����f���v��
;� �)����P3`P���N���y.�C��q����$P ����J�D���_�z0T+@����W��s��9���L%e�t�����f�0�K�w"�(�� ���i#�7�������6��!������Xi r�A��J3
�B��Ew�^n����{
�|��[���q��-qa�q�����0x�()5hu� �4u�lY?�������
�*M���&������oq������fU�������?G{L7	/N���\H��D�
�.�~�}� ���h����B��������v����-��;�{[vnw2�����������\������J8�Am2r���/e�_�I�z��ng2��b@v&�Q�9��J���,������� �����C��n�n	�!j�@�����E�D����	����0R�8�����=��&�R�/��������3��H���|��l���r6K�c�V��+0o��6��ua�D�L��6���&�+��u��
������IE�����{��8����o[$�g�������o�s�Z_�RKSX����4Sw:�&|m(��O�
{X5�e����O��,jC����XT�A7�A���B-������5Z�T����%#����3���d�"���(r1�y���WR��eG��-T��U����$����?����}�J�6Z�6*�T�N����|\M����{�@��R�-����W�Q�l��^S���f�����NT���������-�4�������
�4�[�e{��Ygd��|	�0O�7o�3��}B���6,�[���~
h���3W������}RD�)�9V�%�'�s�%U���4?.E��C8-�l���k_�1���U���D��w�W%���;�[��Iy(�T[@[���]n��t�x_\l�����z��Y����b�_9��5CK�,���5�)zq1����5�y��0���uc������=���-n�D�����N�r'�Vvb�;A����	"��8�N��D)w�(e'F�z1�R�j�Q�#���9.�l+�]2�����C�U/�8��G��A��zL����0H�����2��l�����O�x����#����L�>:����Du�h8f|y86�Eo��`df�+�zks\���)�������}�h`�h�z���I��S6Q�W�p�,���i����f������vU��_�����������~����=���x���H���,@����~�%�R*�}��T�j�e�Q�YT�}7��)�
�����M��7k����U�{�N+Zl���CrH�u��o~���&����
~����������z��c�OV�J*t
e��1ff�5�����,N�����w�<#���&��b�zKB��b�<�E����)��3������V�1���	�f�T�U�:z��Z/_���9�o��6Y���������������|�9��l[����_�5��u��{b	s�~������`����U��������r'.��p�������FE���Mb���^cS:���.�GUZ9���������Vl:�,��1�������Jy��e��#��f��������o������w�����o�������S����#/b����������PT��eEv"����9�#s������O�6~d��
��{��|���S�W�'|J�I������i����8���S�1tQ���Pk�S�����?,3�e�B�o~�{�.)��v���;yj��u�����4���zm"��aj��S�?��*:�$�=��=����m��t�{%�=D��sdw��N���i��W3��Ii�)�5'h�y+��6���O9UL��S\<�v�c�����O����������:D�O���z���%�=�����ZE�\0���}i:s:�����9���+��UCd�o���Y���:�@�Z��3J=�b������hU:�0v�l��HB��>~�NG��_��:E��d�(��#����i�RF��_�������~�����������=�a���sTV��z2K�.�zB*����}��6�����������_d�x���FR�X6vy���D�,��,r3�l�2}�}�W��4�������r����x���w���,��c����
$���g��p��tJ�$L�������8��T���	��P�G��z�,:�pX���-�X����u=���Cq(��k��2��jS�a���~,�P�y������=U����`"��O��hy�	���hN��Uy3y�;q,�cT����ANQB�#z�\v�����[����^�������`����^�!�P��j�<�������K&����qP�5#GK6�SZ%\��y�����?lx	��>i��"��_��dH�a,Ps�U�+P�����,�GN��d7�`,B��0�B�6?��W����6�P5�
����"�~G,������5�[�
��,�o�W�ai�PWQ���#��A4�3�����z��}�L���{"�l����<Z>v���D4��e��)���Z���O���K��_9����������]�U,��F��_?�����q��A�M����,#;�VM���TA7�_w���`d/g��/�!�3�`�6���������O�>s�����#K���/��(�a7Q���$�����=���.�5(Nf��%*���}M�q�67�;0����~�*6������q�[/��d��=G�6��<^�_�����}�� ��M9����K�/�C~����;�����g�w�5o���;�D��:��j����B>�������i����?|�c���_�B7�P1�@�M�8�%�y#����~?��x��p�h����A�E��*��-��[����Yh��������4�&t��`�L������3�c\DU^�1��4�<t��`������'h�]��hv-
���g����a����9:$����s%
�j���:�=������Kk]��<��h���g��M��g��|�DRV����c��D7|�*aU�������h����
P�1'd)��YM�-����jkm���k�i�K������Jj):n;�G�����P�7�T��S��G����b�ZC1����~�p�7����>$c+^�C�t[�G�%�p*"�(8�E�n��>"K���8t��t[������[6�i
�����h����Za����K�J����,���5��b�/���<�=��,E�s�ED'yv��ml�.��M^�����-R���!i����m��'���(��F��V���m�m�tSojU����TyC�� �64����G���|�o\�q�s,�-3�/����o�sK�]�$���ys\�B�$��� n���re�1[x�<�F��t:*��K���>�����/��mGloc�z�X��"��>��<�����q��z���N� �VAu�Z1�p��a�K��/(�+����B��V�B~�j���o�X�#��b'S�����2y�?GI�a���� �AZf�i�����q{����C�z�7�>fG�������
6����e.I�H�5����t$��4�����S����Cb�\���\�T[Yay�bf=k�s+��������
��2VV��;�#d(.��M�sRm_6���j
`�o��#�Y�-�����'���1���k5���IQ8#/�����B�&�Q
���t{����x�e�|�����v���-��4�"Q���y9��� �EP�Z���7�6�����������p�_�$�g��<p�E�=�C���c� f�>�����u:q��s
����F���pj��+�r��8�??�1xC1����<�o����#��[1�OR�PJ?	lXT��*��{�A���g���*�����=1e���O���wMXst�p���}I�	`�C����O]x���[�������]�fR;T��^N�l
p{sEwi]�fR6��>�g�)o����yR��?hkl(rNr�j��/��CMZ�X�j���DtVd�xo�-F�{_�m�o�'Z1���4���-��7L��z��y�c����Vhn�?$
��Kn69[c�yW����l=$�	�%��E�K�������&*?�����P��&��e���5���B��+*0��F��l�U�M6R��������W�������3"bx�%N�X�rFg1�����".OK�KY(�D�V7���L �*%<���0�n�(�b���wS�+Y�]�����L�zK�����e�iq�G;�-��X����)0����b/,���~Y�uC/������?o��31(��������Y������.�V[-��(�a��$t����(��!{a��5��������gg��A��g�em����v���b4�<c�B��*�6Pt��!\4��g����/�nj���R;�Y&?����o%��
�J4���(���b��=)wI1�]����'��7g����N�����_Y�D�������_��K�
��{��d\�C)����'�&7o�<��A���T�_���������1�	�E�]��TqpKV�����u@5��B%`�s+�4-��o*���_R�������4EgY�4���$���������R �x+b��#�*|������H#�(�����BA���D�!i�c+�i6	�P�&^����*�1�i�$K������K(N��~Ea)�4�Ba���%e�p��y:�R9�m1D~I+2�A��V�IKQhZY7�R�v�Du�Q�q||�!�j�j�)���2P'G��D�n���S�/q�Jq�B�����n���u��)�J���~H-68]R��odS�,�M|��/20��������o�mK�}�{�����7�O�u@	�~�����T*�r�Uq���>�o��SV%)���&�W�������s����������5����=�S��b4�G4c���Q��o�Q���%��w�r~�����S�|s�_�/1?�^[7j�Y�����o�_}7VlC��9�����UoE&i�{�UXq�"��N��xz]bd	`�m��}���c���H��C\%[���-��:;������d��S��'�X�=��q{�2^�1��4�|�����$�c
_��
k����k���J�t���a��n��!RM����E�4����Y���b3�����Q6�����B�V��B���c���-��H�����U�KR�2Q�� �����w6�tEa��t��3]��3]���CD�"/�~N�d����;���[�kC
�����&�f��{����� �J��&�Y&��T���Gq�Ek���@�,�`��zz��?Q���ro-R�h;�s^q*r��_s�Z[�{������,~�S/��|@�pl��* ���5�m6y������`��eZ��Pi�b<�l~ti��L_V�?D_(9��FV�5�3,���/����V��%���R��0��&�Pr����{��{��x7I����z�}M�C���d=Ov���i��I�)��I
����D���FQ*m���yWA�S�$�}��o�w���	�7y���z�
�>�x�_����j�z�B�)�7���M(j�d����Rdd
�M������|D�~������!�����T/�O?���!��V��f>i�jv�l����O�z|�������)�w�_��R�E�?�;j5�_��7Q�~R
���7���Dk�A��Q�;���\���&��Q�	x���f��x@�Z��Hc\������M��-� ���+~���j�|%��5�����[T�Z����y�l.�sc]%��5��K���Hm�~����s����J#+��$;J3��bIt�p�v>�_�S�1��NNW�+���&L��u%V����$�[�
|"��VbUZ^��Z�
"��VbUZ^�"[�
�D�m�����;���Jl��nq%V����!����`I���J�
D�+���+��+*��VbU Z_��Z�
B�VWbU@��J���+�������X���;�����gD�m����|���Q�)� s����J��4n5�;d`%vN�Z]�����Wb��X��T�-�����Wbo�O�As�5�����Dk�]���Q_�����Vb��Q�����6�Wb�qM�Z]���F���`�8��`��V�`i����`����:���������.� k��{k����k��i��,��m���\���p63���Cn�SV9/Q)nS����/�
N������+Qr�O���1������}*��,Al����������)���(���%�������/�".K~�OR:Y^9�cc�*���P��j}��C`d�-S�oVX�c��Q�9����F�*J�e��S*o�b��(��`5�q����t�
���5��d��l;g�&�G����������Z�Y+�j��-�K��1��ELU��K���kp��T�����W�S��
�#�X���Z�����._�Z�}k�F�h�B�R�
���g�}
�|�&��w���2:|���2L�eD�4Fs��-�q-����v�5�X��JVH1T�m(���MCy���R,5�[�^2��v68`)�L����:����������<��
'oQK���YjSK����lU*��Yy�f�M��j��������$e�3����i���������I��U��D�F�b�M����0m'eD�=:���Q�i;�"��k�n��}M��.Ot����^�@��=w�Y��%��j�29��u�o*�|����qd>k|
x�o
���8�qs��������g++(K]sx�d�`�J\#������lk+���N�d�5=kXi��5���F�h��v���~���V������U}��TUN�}�������
�~nU�0�g+��m[�`(����F����V�������5,&V�@�i5O?�����J�Xj��{/[(�T�J?JYJ�������������v�M
�W���������&�6��@~�mJ�/59��
������8_Q9�����q��mJ����[��P������z���b�Z'�x�����K(O�1q��>�H�_���u���9���5����q�$�O�>��K� m���^w���CyB�����6MKw�$e��E�������_W6��lc�KV���'6"��=����/�[��#�����������P���j��}�R���Xh�>������g���_�}���}�E�;m�RF��<�������W��kN��j�����R��������=���E��B�m{}��f���+��h��m�y�a���PC�.�l��6/��^f����s�M�Y�M��bu�����X��Qc�4���c�||N*'M�8*���H�fWqq`?�9Oo��o���a��1J�r�\��Q^q���b��0��������u�$��{XN�{����D ;���L��-4���X�%���&��R��x���\�{P}�Y�[�A1/��9FP�&������R�g�(���4��0X�
ll
l��
.������`�W`���v�<Ug���W\R��w@6qNhf�;Q#���������N��,4/w��e�q5]��G�Z�Y�z�b��m .1��Q�EU��G��j��U��"i�Q������`Y�6��O�O��?V�]�j�g��{Dk�"�W�h�@�F�v���5��m�pj�
P��8G	g��^|��60IaI��?�EZk��t��Zkt�k����;][���	[��vH�j�!�l�57���������P��*��y��U�it�8�{�;)h��0�E�����4��o�)(�F�	�g
~���i�81�EH��k��O��}�F�)����d?��O�� F�)m���������F�)�1Z%1J/I��U����P�A��aR�^tJ��7�6_��!jP��y�����}��Qx,�m�;q��0^�#��/.�R�>���c���a����8z��\�O���_R�4��3I(�8ZX��n�����a�`"���b�"T"u��]������H����E��T��� �W�([��
�(�n�M��1���*/�,:���]F�^-���/���		��8R�8U��?$�^�O���[RN`���*���nj�1G�B6��R%�����4*K���o�RPv1��!*���#&'h���0M�T�5]�"�HqY�GB�li�e�'��v���������1a �����:2��,B��Z�#��% H���C��;�A��"
��@C�(��� ����������x0��fBR FA��x�7{�2Nn�g���G��Q���8)"�h��p�d�=��(��#��(�4Q��^����*���T�Q��QA<�����7"C�L��W�c��=��X��	i��Z@�����8i6�kc�C�
�JD�&���i���f�y����{�R����_���e�Ix�
h�����r�w��|���*������?��a�����9�����s���c��E�&
�?�q���v>�RY�n^��wb&�K|��Z�0&V�����������2�|������`�[:�8XP��F���5�?2Nl�0�#�F^������)ba�X$�I�Y��8���kG������
���/�I�ty'Mk��*N���+�����a����<v?�mN���i�����d������� �4Ov������A��dGj�������E�
�i0�I��T�M4����� ��f�OcM����8�Zr>�%-i���OE�j����$q�I����$~��F8��.��O�������^��'�Z���&�����y����v�\Mo��d�
t5�Q��A6���2���p"8�1�
���=�-*3 �F����K}*�&������ ��V]���&-�S�%�t����|�oM�&��&6�R��1�i`?����e;]�F���m��^�%'�z���&�����y����v��7�r
2�LmT�z�Mz���OkQ8`����i�.z�[Tf@��t1�I��T�Mt1�A
kA6�������tp$N��\NaIK�����Tl�������o��IMh?��w�v�������r:{)���j��.Z�U�������������K}������u ��V���MiE8��b����h�:�ZQ�7*�q�Mm��.pS����$�l���%w�q���o=+���5���^0���Z���.��l���v3
�m�Q��F��	����`jB����i3H�/�g5���j���3a��4��
U[�w�E��7��`�Om]8�d���jk�N� ����r�
L�w9%x�
�7�qM�L{�����,-]�c�U��X���s�s�*7���]jS�7����4���1o%��l�����8�gbg��s�qa{?�M�dyJ�
o����������5�����u�Y�XNM�������5��u��o�3�}����$�Sj����<��@����k,���I�����nq3`���
�n��q������)li�
B�Xt������aa�l�5�#�������_�9.��z��7�4��.����n���M��'o��i
iX�o����V��W\2@L����hAU+��T�zj3.u)���zZ#��d�[
��MaA�&��8ZO�U�
�
Z�4=���pK-��F���"v��n&�^���f���!g"�I������%G^��|�@S�~��y�zZ�-u�G���������y��t��7B�kNJ654��_�r�
��F7��5�ROu����[F��f����F��pgP�����\a����B&4�RCe�q���n�P7�����q�� �K3z�����T�	�n#�!�<���g9F{���������4��^��nP���������m���������0`{p��"s�N`�6��pm���r����6���zRH@�����$���}c���CNn����n�!P��a�f��ty��n&z���C�)���+#q�RU���v�q��-����t���))������"v���y�����q�K�sR�8��p,��L�lt����jHo���u^�g��f\K����pfK�cUy��.)r��S�Kq<���Wy���z����F
�1����L8�Am�3���m�m�-�L��ZA����i<QF�4��x�,�zH�����8����q���������}(�4�V��e��|��K����{t~��C�d�����vE�����1��Q�'�_~��
�G�=�d������>7�
���d�g�@cR��{�Z�q�E�f�����S������z��8�v�5��������e�M��_j��������(����/�[�g�������yy�k��y�g��;�#��t���7�v�0�r�S��G��OYR��+E��v�V3������H���5���j^�6U�{z��7h�����gR0�`�ym�5K�5>��`��Ur��3og�#d���z�Z����n���7P�0��A|���c��v�����������s����SzM��)IR�q�fu��;.��`�/5��<������I	������� �������s��Y��������W�PS�@[����}�w9K��/�uA�E���[���1��kg�*^���$*n{���Or��!�������_0kz�3�8F��h�������-A�7G<z�TF�z"O��j_��j���g����������������G'4�[]g.K �6/v��{��'��|-��b�?P����CA�6p6L���G���a���Ta��������b 
CT�U�`��U�_������R����V�a�J�}����	��� L� ���-��M9��/�R�(�� 6%"
Tax��.����g���lT�!��0�7bPNA��A*Q�:�L��W+6�aQ��a*�+������PC�#j(�>����\�`�A5T�KpR�Q|
��Z&K���P	������tR��sH��h������
;+�4������qVmz.�mH�������G�D��<� ��\~������U
b(����e�vp�g��HC��
�J��*��H�����!�q[C�D����<0����:=1(��&A*Q�8�LMEB�Mpd�q�J��~�>�b����O��O�RPfq��*���&{��@&�+�0DZ=��?�����x-�	�NH.�<P%�����!@���c��T�d9cU��ij:�6B��f`���)�������Y+��h��9[���Qe7�v4�U�N�VF-�i�J�=�E�}%1��r9�rLk���U����Y���������?��Z@�Drx	��8���d��P�q�6�*Lhzbs��7������]����24���*���N�f���1����U�h��9�]�e�.�/���+a�:�-@V�}5���}����a��@} ��W%��Yf8��a5�T�d�8���0v��)hm`� ��;��_�A��1��C\��;��X����k�y����r��;��x���8��������kmc������������?�0�x�k@�[Y����b�v�Z��X��������w��-f[��|_D��d;�F�d�����B�������>��3�kx��[���/#��K�h������c�J�O&r:d�Ls����|Q��#����)l��kc������mV�Y�z�4��bIcU�atg�2(wX�j�Ah�W������3���*�r�U�F�`(����NdU�at}V9���&��R�?��8�[���������N���bi�<k����y$]�ys��&�6���,��
�C�7W���9��0�u<�<&����������D�{-��}W��]E��"�b�U����3��3
����p�!������g"����xh-��FCk���4Z������Zt'�����;i��Ic�����i\��O�(��/b���FM��7!��	s���� ��Z�_Z��a��X{0+ma?��b�L[��Z>�����k��r0t���ia_
�O|/���z�;m��#�ic���N!-�Gw��h1:��FG����66Z������f\$e�K����P����<<~%�Lq���H��Z..�����$��s�,��^�D����������*�p�������xfxF����1L '�G�7"�XO�����;i4��Ic��H�N	��Aw�8h-
��FAk1��4�����qu����m�$�q�L��Z'���+��;`����K�M�����:��K��w�Z�zW���gh	d�_b��;-��K�`y6�&�'���(i1F���H���6BZ������btt���c�;ml���#���x/�����[M?�g9��r���F���Z���I��D��T��[���(�=T�uT��F���������F����^���W#��PA��W��wP!�Ray=�����t�ZX�
����;uP�R��C����NPm�Sw�pj;��SS����<�Z��y��?4c��b%���nb��vxCS��BLWah�T��ZC�	����"�Ux��M/��
��
����Hs�4i�U�A��:�����!�v@u�����;u8�L�����P�NJ���3�b�����wE�����y>e[9{;2?;�Wi�b����2^���P�m[����Y��X���sy����BuSgA����N�0��(F%b��-�4'��lc~��r��W��������.���Mqe�a�J�6�
��3�������^������B4P���*!_zH�p���|�����S�sY���<4�`N�P�A�J���t
T]`N�P�1xJt�5����3�B���
T�t���{�L�B�����S���5�5�Vy!��R�H;I �$���*���#�A�\_�{���$S��D����~�`���h���� MBT�5h��bU�VJ*�@%J����K9j�������R�A�J�.����
��3�e�����S�S[N�OY�9Cf��F��!������@
�T�w�C��\���k��H�%���W�����)�z�����FMaz�g7�]2�YA�J��G&���=�0R�d��4y�4**�#B�=��k�4���,�AM�Y�� �\+�3q��M��@����N�����-}�ev�Znu&o,p&�T[e�Wis�����<�"a����pjn���qA�f�������r�'F�� .
+�gQ�#4r�W�%�]��Q6P��G�g�N9u9��_,��CW�����T�� �\��0���S3%Za�cB4r��D��E���Ms\�F�]U���`�D,rL�Fn���sD���K(}UvP��0�i��M���K�VYA���[�.�����O([���|��u������k�j��N��%���(���Q�����rb��'=����B3��l	���=
}�@6�p���A�����s������[�tj��Gh��E����vm�)S8<#w���K��E��+�CQ�#4r���K�P����P�h������#��B6�d�w](����3?���]�����]
o,p&�T7�}P#�;?H����;�-=2�z�Rd���g�J9%�Q#y;��E&p<xF��r���4{7��E&o,pF���Sb�s�(���9��f�)�s\�F�]h����3�#����8�119v�Af�K�H'����+k��i��6r����|�`:C�aiu8
��F�^��E
ZR^�M=,X
K��q�c�4r�F�M���ipvP��8�Ei���T�SxM�x`����4�6���m����9(>m"
��`�\��8�B?M��y�Ji
�3��H�����{$��i���P�������lJ��'����`��8���`���i`vP��8�E�pnk]��tSDE����_��V
K��IH��y?~�����<�_�qQ�8w]�L�������^���`��f
�3��F._7�����i��l������S[�b,�h�k��v���0VH���*���5p�f;1��]���\��7�U�G���y��8`Kp�����SG���.Bj�8�Ei���\��.'�;Z0������-�~�@���������'��tC��	HG�i�`������`4���M3��:2L#�zj������8�QA�6_����&�����{jz�zjJ��0Gi�����(c���by5V3�:�vt�Fn^���cx�!�D1��@�\,'K���-�p�t�t��!��0�qA�vSb�/F^��T�A���[W�-?�����p����Gk������z��gxn�D�_����Z����B[��-����
�.�[�k�nOCO^~��c$��z�9.J#�6.����������2�����f#�7.
&��3����n	�7��P���l^G��u�h!&mr�������k0|�i��\#wW���%mt��	�}�$�r
hk�����1 �R8��4*K��)X�Fj������);�V:�P����^�����������L�Z����*']g?{�F+��U�A�
���c�r�h��C�D+�qD�FN��j-�`E^�fP���Dh��E�G�z��)q�"��C��Vf�I
�9T�4$]h����cW%��:7�L�U����*�5a��-��8���0�Q19vSB�G�a���
�V��M�����.�.d�B�+�N�n�,��0���j��� L�6VfP��0Gi�ZU��O�3��)N^�A=���c�29�r���)c��S�,�����>!%�5R��f	�-��@�\��qS����b�nu0��0���T�,����	���[����*!��-n��0�9u]�$��P�S-�rT�F���������2%Za�#"4p�b�&7i��q�^L������6�)��$��#5r���^G!��U���.& ����A���O���+���nu2��X�����e���O5K��)X�Fz����n�}��=m�1#KN�!�����4r�[b�W�����fP�� ��h���,o>�[�VsWs|��&�-
��P����!'����r<�iPGzU������>���~x���^����8\�]�Q����sNX�}�p�������_�������KX���(f������ g"3�A��1���nj�>2��� ���,�/��9��p|p|G<��M��e�1�#�$��57���(Q`[]��B���4�y�bQ.�lX�T�a�ZYTZ��(x��sU1�!�y����U$��S$�Sg>��;����C�����#���u����� �����tW��S����`�]H(?��Y�K5V�����%���K�8)���d������� ������d���xGt��Q$%B�	���z��-.�����O���n�LZ������f����P���=��U��r�9a��b�'�e����N�M�2�aL��|�|��|�u�x�������u���o������Ob��\�z��o���g����%[Y������m�s���k����$����2�E��s�W��Vy:u<Sg��hR����~�]��?�3Z��/��������8��/!?���|��x���P����8��6���������n�I|f��dS�R��8b[��%ao��i���>G�j
J�����������2��}���o���������o~ �F�u��8�KCi������Y
����m����'�2����f"���.�W�TeIp<�D�D��mW��Q�5��,�����j
�O?��kG�%u�8����j�����+�� �X$^C�^�(ba��{uw��fi1�`�mr�i�Od�~�7�$���]���o������^�u���OOw����[�b�^2.8gS��*����$�d��"���r�lS#�?����.l{I�#3�roy�_������}	X��l.���/���������#��hL��D�%eU:���q�l]���$�]��E������NU���s���|�l�RT���S��#��a����y����
�6�B�;u�O�r�O����D�"���4cC�-�z�V#�������Bo�J���j,@�-�f�VS��� ,��w�r?I��b�����*���o���W��M�d�E_���"��R�2�-M.z���������K�������U��b[�����x���mzH���/����z��~E��E{	@65�z�������	;rsi�^@W,����Q�.Jl,�������0^x��������������%~�r^\l��~?�����U=ts�D�6��S�e�����]Nb��@���K��TU8n����
v\�=X�8���]
�ET:����c]R�,��'�t�*9W�BU�@���A��~��d�Ok~W�P�S����w��UMY�q��$k�c����@�������v�P�
����X���|XU�@�?��>;?����t�����kmgG�0�e��jzy����O��Et�p~b��������?N��p�8�[��ek��h��������m���5����7Q6\l<�?������vaZ�����;�"N�
�R�s)Iz�=���q�!��U�'o�_���8�wq�p��_��Ww6����#�l��4���M�h���M�h��v��^�O��|�f��N�
��`	�f)f���uk^w�����X7w��W����x����Y�pi{�����v]���y[��q����qrbI���)H�i��"g"�������o��W�)�:�[�ls����k��e�z��^�����n���]���]SGY�CG��qq��8n��"{�^������7��7�Mr�v�i	}]A_[������H����lo����*�����m�^���"�������^k��m�E�#���(�b��T���1���S��o�ce�����Q�A��}��B����SqR�z,�����o7�*^h�ty�����VXA�@���M���&��M�Y\���\�_��r�;W�� ����� ���5�[l���v;W���O3������\�����q����Z�8�J��'���/�hQ|���9�����/,�i�$�#r&r.T����
-��q����2�Y(�d�jz
vZv������Q���V�i�ow�k���$,����rS���'��m���	�z����h���E�$�X^��v��k�$E�����>H��)�&v�i�7��.g���/\�w�~Eq%ru�4N �^3�I]}��H+[ ������[B�l�h�{3��6B����R�*����M$�_]�����P�����-��\RlJ�*Z-`��T|�������:Vq���;���Q#a�n@���%�w�<������wY��&C�\��� �����d�^k�>k2T�e����c���&��Wc�V��z+K}�d���RO�/{\��S�??���� 'g�t	���!+UA!H���?�L�:Ch�'/�}�"��-��J��6����PD
!PL��H�~���k�F��"?�>�GK^��P���i��U��P�[�i1�
v��sd����	���"�	�M��^��%��
�?���R���%����5�o��Zo��x�@FXD]�c�����d"`���9��k�F� �xL�e��Ip���+��y����r�0[ByS
6�!�&0�}�Q;\]��J�d"����4��g��4���q[C
��NU)��1Qf1Y���rrD��0
x3mH��������KS`�1
^+h����6W���XN��H�8���4Di���An���C����1�]�m��6��c`yk�����s|B����A0���1}��3'�fU���!���Q�����y�z�L�����+D��8��D%�'����Z���5����va�5���e�g�H���3d��L[���^Au�r��p	�s=,����>@�<i�����}:�x�f6��fI���E���{�!�uP�Fg�x>�|���O_6�N4��`;��#z9�e��r8��_�8d�C5�B��#:�9������=����);us�Vf��5k�n��<���H����0��4��U�\{��!D��x�V�a2X4���A����'�S������<�z�%	����6>�������b
��D�� i^w��~����u+�~����L���������8����8��Z4(��[��%L�:��Sx�D�?l��@���FD�vq�D�x�h^����swZb��2����\D���"��T�������V%S��Q�}P&:��1�����j�sj�
����chP[_U��XxoE�o�}uT�u���x��I�9�w��5�<�^���[����V<�d��VT�p�����j��[��ma.��_��aR�7�_��/�\�\o��>���MaJ��:��[E�K'�n[n�W?a{�
H$��,�oh��1lP�ig@ZU�i���zsy�x<Fg9���m�{@5�I3�2Fj^d�����@'88����2^w;s������^�z��$,SN>�^W���bs�����a��Ar4l~���kW�A��=��@��`�7��K4v.m�"�A"l[�}2?e�����2����G����2��`�r�s�cV��=��:n����h�k����Ko����<k��9�Iy�W;l����b�c������V��<�Q���1l�c��
7���������Q��B��.#�Mm�w��
�A�@-��g��W[�
b���!��B
���f���<�[Y�2d$������Xg4����;tQ���e�m�T�q�]���,�B��9���O��I���4����`�rXM%���*��[���E|�&JZm�������;��V�
'q9��1��Z-w>�G5FL�
T�x�
�������y�5�d�5S����6������������.>�Mg��hRc�s#�GQ�c"�n�����L����n//sO��o?cYs+�t#b+/b������(�~x��`�g������\�����%�����oY���x��dIM�N�nEV9���)7u���UB����
��������EZL����	��-���!�	�(wo
�)���.]a]���:8<wn�����v~63��Aoj"�o���V�Qa�-o�w����@^P\1��x7���$Y����1;�\���/��'�vC��%��U|\����No���K���-���4������������R���x�z�}��f&�X���mo�`���^~��������.D��]���~�^�g�s�,���K3�srdY���|@G�*E��%����zB�*��C7\Y.sw���;Y��"�E�}��4����B�(}	B���	��%���(���K��	�A.�����K���+3���e�j�PQ��=]�$~O������2/�9*������R�B�JL'���]�b��^v�%a���O�+�E^5�,��,F�N�Q�L��������E��v7�QsM��?]�<_�zo�9n�G{���J�
l��}�~A�7�����A��AT��D�
+��WT����v��1�1����T�9�v77|�, N�aB����p�����t��������G�Ok�=�})v-������E#�|-�����d�$�[-�gb���������d��T���4���S�<��.p��y2_��&��Q��Zs/f_�W��p
_�����GuG������"y�U�L������^��[i���&*s����[=aU����
!�x�(_���y�!����y�_D���g��{��l�Y'����M?#j�R�]&6��x�����������������~��w����Q��x�����?|C3�������1-�bc)�R�4���d�Gn��8.���O���{�(��g�������T��>�%M)�bK����VP�f��{{�bz��|���r���=�����.�w�C��������!�'h�����jH�lp!��e,9������A��oq�o���q��v��}�������>lK��N=)�$���_�-���>�X�/������E��x�7V����v^r��d������y�b~�+7^����������*��~�����EO����g�P����iD������R��M�:/#gB����������D^~&�d����
�f�����ut�����y2��8�.��q�L'�;p����=��u���|�;eP��,��������@��HC�H�f��H�bn����Ae�q������~kX�=�)���f���T�������6����Q�����=��LW��+h<��M3�CFms!���r��3��.��.=o�)T*���vh!�3�a��2=��H�����h'������Hm������;��gI��^��v)9y���Z���	��qP��D����� 1���M�2��EP���-a�-�a�S:��d�������`��U�7���h�u��.�\�p�������u�'����_�?����FjQ]z�s���p!�zF����O�T���`��3Ms�������Q�'�������.��Ng>����\0�N��<h����e����G��T��d[9�A�����>T/�o7���E8�v�^��xz����V�������FG���0�i�YAQ���v,P�aQ]H|m!�"�GZ.��S���0�:���=�i��J��$��]�4H�
Ve�rW�VA��b�6m��7":��j�{g�?[V�Ul8�/K)�S�Wk�-0���-������7V���i�!��}���������Y�������;�����@!c�Y��=�W��+�Nh��n�����*��38���J^�|���a�P^�N���I&��m��G;���b����8������X�Y��-�m�f�{
�~�����g����o��5����N�i�P��G����a�����d�,��08|�02<��W5��|2S�(B>���s��Yx����3��9�$,���`uK_?U3&D�E{�M["�8JZ����sg���q��A�MQe�L������ ���l7��>$���[
�5��E_������#���+������7�e�p��cWG����'��)���[�[(����W#c����Dl;�b�\��O<>\�g�_8����_����{}�(�}��b�^���i���s<�Z���jB!�0
�{���FW��Ad��4�2i<H��P&��
����/����]���w���&O�@����mjF
�������������`����7�_{�������
����u�z�����O�iV���E���w����,.�|A�%S��-��!	��`�h`�Z�9��Y�V�e_�p{�����fY
���3��Ke ����8GE����2G|^<����c��K���]t�����y�����Q��
X�	D8&>������*�Yo��1|�(�qT�FN���C#�WlD$`�C���,_�T�2[ 
rY�d?[�~FA�J�G[�<��\W m����M	2�������gw��x��X�$������$��S��w
=Mz'�����w�=�Lz'��S��w�=�L�H<c���
H���~������K��P�����I�W:��k����)h.E�kmyg������jL~9��P1/AH�wR�(��h2�_V�=j
"�=�7S��Dh��j�����p�z(dau*�qL��S���2�����=@\)�������W���Kmr��j��|��iw�Bu����\!B�?�,�F��z�qU��jx�
��%�#���� ��!�����q������e,�r��d�P2���I(��,�r��<Z����BGi).�p\.5��%_F�/W�S��\��
�)@q�5�rv@�6
��4�Cs�|9��/G�����r�\9��+G�����r�<9��'G����Y�B���#���FCL}�!W'�����"� �q�8u��Bb�|;E�V��
s|��j�l���r\�B	�+U0�q��Ty
���*$8~�j�=��+V,�����-�{�����k�j��d���xF�
!���Y��`2�GFh��jSe�mbm��+�8r3������c����'��x	��l�71V�#�3r���k��J�j�"G�g��u���&(bC��.��!e
G��`-�i���F�#DW��["<ZAx��*ZQ9�U�*ZAx��K�rs:��!�S.(���m&}����f�7���i&}3��yf�7���e&}���9f�;���a����+�b�)��9a�����7"8�U����B"�b������k����j{��C� ]����`n�d�������17�w{|Q�~r��pe
�7�
�2?���s�\�8���u��[K*��_�a�����*^��x6W�1���0T+����*���w�`q�?_���1>��A����T�$��%W�"�Ze�;�qD�����J���������hw��7Z�L�/��e$��.�!��J�T�{�|5�#>��nk\�H����}�/�,6F�k	mY,]|�ya�;c��e�;����I�����(��GN��Y�G�7k�-<���[��R�T���3"��*y���m��Z�PG�����{�+|�!�rt�&o:�jeEBB���c��^����,��2?��:iv9:��;�R-Rf��eqrk����~��e�
�2��sd����[U[���Sm��\�mv�!��8����X-a_(��N�,
�iv��E��
�,��:j���=�(l8����,zg
���|R��j�8r1��B4r�������f���0��a�E�����\(&(�92H���j���M��0�qA��V����HeG�H:6L3��j��ia>��ut�W���Y��]������$F��P���tWu��T�B�
�N'KP���K�6K���,AE�Y.g����c	������kV����lV�*w��[iX���x����&�$+�j2H����&�$*ij2H����&�$))j2L����n�>lo���]y������+�0��c,<]N/jQ�>��{o�r&'����'�
9NBC�id�=N~��A��D�89���n���~�x/_�'lQ�q�5H�0��ln���w��\��5�}|�u�UNU't��S�^�_~�'����s{������b�������-�v~f�W#	������b�����e����5�'��	^����>���"��g��NF�@��������	���� n��(�xj}�"���S����*���*�+/l�N�%�qy�_��Y�z.������V�.(��\��J��R���G��9'������
�WG�X�#k������2�,���� �\��������Q^�u���� u�/7Y|���'�����		���&/� ���M�T�b))����fX���(^kh[�������>v�8s^�7��Qcsl�*o��R�
9g>���n�z�B1����toQ���	�V��0����5k[���Ll�b,/Y��)KC(P��g"P\�]��#�B�N�SQ:(JG���@1:z�vd����?��GO���)��b�������J�l	q���1����a��[p8�M���Y�2�JA�m
5M���N�����0����%/�@�4	*B�n8Q�����5�2AmX���P5��!&�0�SJ��y�+�/�L�F &l�J����&��5Z;��0�$�Q���+��$$�����7�(Q�����E1��z�8��F-�qT!"�q I��L�J��7�m���N=�dK��p����4�H��,�.�=�(5����Z�$�M"R���bmlD�_���?)qu�O�0h�FP���(K$�b4F_�M�u�7�~=+�������^�[��B��}Z=7j�n*K\R���w�v$h?f_�E����&��NgnQ�x����/�6M��^h	��������s������:���X)��m���S��t��;�]w�&���lng~r�U�A��F��{�\�����Nvq=�����r�~C�Gq�-�#���jQ�n{��K���G����S���r��czC\"q���=�Voig����Ox�
�F��=� t����������r�>����#0���xI+k�M�a��9LD:rk��}���rv�N���x��z�R�tD-�m),����^
(�=Z���@��@��
��4��Q���r�=Z
WUX�j@�5&���r���T  �+*�	j��4"=���&C$��S�d��4~b����OK�!���Ii2HR���(5���N�7����XP���J��b]�e����hF�z��n��tZ�����O�-��
xN�]�!N��������������6�w2L�����0����-��z�u��E���{��:�7t�%X���F������Q�c����6���K������U���o3k�e{{h��	�����0������]I����{F�z�-e	M��1^W���.#�����,a��	)LXA]�aaY��5~w��#t.);\B�O��;U�h�>n�G�����'A��h��p�FA!@�v��*�����������o�c������m�
	��&�|Kz��+? ���FO	����(�����J�{�w7���`���S�
��D�6��4Y����1A�dV��$�v������c�O�t�N�p����b�
�.���'�d��w*�[�N��mt��i�'�9��`��t��@Rj�
����������R0[Tn�fv�q�����\R�E*���0/�O-8�
{�L�k
l5G��w�T5�����LV�+w����t��h�%��
�h�
d��]T!�}��Z���d�_co����;��_v����N3��f!AU��|�V�*��Y[�dY�sz]:��V_U�R|�u��Ug�uUWU���'��h��#������]S8\T��C)Tu����J�.���]^N���7S�`3��S#\���ZUt4f?C'CS��_����������N���YC1m��N�s���bin��et(����3�������tNq��Ne��{��{t����\MB\/z8z����7�����Q/q��p�J����f�����!���_�\?@T,P��)n��p����{�
!�7�>Vsw�S���eF��;nJq��8[��uA_1r���,�jv}�]��)e������a����\��'��6�����?}��t�g���6��@m_>�=*
��2+�<��Q�~�:�i����?j��i���w�G����o	��&9����\>�z����#m�`Y,,�����>p�Y������4���|��r�������L��#���zK�z���[|U����%.�W!tO���S�'�I�+bIF�sd6����`8�GY���X�awYuc�����"5���n�8��*f����	������5�^�0��B8*>����W������A�z�2�D�L��
a�6!#�wK"!o.|�5��]��q��0����C�����
~Akm��W�
p��2�z�9:��0���<w��7s����W�?�[�W��}��!�Q������(��a"yd���������;0Z�����r�Z�����d!�5C��a��E�~S�o?�k�Q����|O�Azf���_��::L ����z����~���E�\�vye�I.�
��B�nt>��R ��n���oZF�O���X��rnt��$�����3+*�O]
�rU|����"���z'�D)��n"s�^-'T�9Q�q4R�O���[���}h	�_�]?��A��>_}>���n����������t��]�#5������ px$�N��C��5��3������ ��4��EX��O�W������6���A������W��`H��J2�����T��^g���q�y��rk)�"��Y��r��7P���,{u7�<�$Q}�=�;�{	����H��c��jV=:�{I���������Z�{�.c�5�k%K%����<g�����b���i�lny�=���''��1�{�jY�Q���_�:$��p-�-����-����
��D����0Gr>�M�M����{����h����"������
�SH�"�su�K�hs�P���@�:��������p:Eo���?D(�/\r�'hXB�S��Xi���'v��)4���t>2Z����e�$.V����Ij2������j��~j�����y�1��`m@mw����������,���m���(f��B
7�(��D	�YC�XZ��sK�����6K���%�O�����P���f���j�23�M��J'�iy������;G}Po��n0����������*�bv�DU��e��%ID��,����1�;*�Nf3���@����`��<��qI���^��%����[���ZO���� ���*n<&���t�9sl�������v��_�0������V6��:::L#��k��{��E���p�u��`��@�\���NpM�����DF�����
���������_Y�
",/7Xn�-�^&����$������i����=��)x��5r�w^�"(9�X1�y
[��)���5r��
��Np���!]���@{����>_���|/����T-5�q)4��3�Sbar7qT���Y�\Tx�q]��)VV`�b(G�h��y�t>��l���`��"4s�W�
�1�p��1���hh<[j�����]���Y@��AbA��:3��0�}gUu�O_���}:o~xa��Q4���Wv
0f�9Y�Y��C�������Qe�De�`\�U~�U��m<9��L%�>�3����f�!�
P ����ttwC��|�x���R�]G�C����Y�E�)�q[B]�l&%���Q�x� ��t�K�+V���Y�I��q�#3������$����Q{����[��B����_N�OTs��5���+��$%���KS�f����'�qU^s�����a�������[N>\#��M+���>����m���%`
����j���4��G�X��K�8n�.��t�j���4F�����_�p�UC�v������Ry���*�o�}���IP�~�-�.������C���}��u�3CX���iN���#4S�(�S�U�n+��D;��O]�nM�&�|&m���Vez�������O�#���A��r��j9�^�����|�����5 2nK�o6�{kgZvw��������L}�]�����6"h;����N�+M��}����#>���R�R$�|u=���������\�����@"��Xy��U2������s��\�b��Lf���F�@)��W��c!;�(#���$��6�����S
�g����FS"��-�����~����f���J-��U��Y�!���M���'�H�i7�
�����a2pjxxb���&C��/�&�&����m�j^�(�hC>�;�st��O����mgz�-��A�^����B��zIk.���t�������p��t��[q�h�%�I[F'=�<�M!l���P_���p���^RK���E�vu7,�4���%�`Jmb��dMWM�!��]�P�.8�����G�lq!D�936�{5��>�'0%�;�*k`�F�l��k��c��A��0'"��U�M
�����_��WY�c��	����<�G!Ys���-�^T�x�:i5�HA��ro������lzK�W�Q����w�	��ktc��6��%2�v����E�z��?�/���F��W�N�)��wd��%�����e��4l����m,J����,��*I;;��F��fq�	<�]��:
�����@;L��-�DT����$�&���c6���V�����8����������K��yY�6��C���SSO����_-�*�������;��aj��0�3�@��Z���#SK5�5�CT��6�?Z�y�aj�����`��Q��������,��P��VWNIc'��	i�t4"���&C$��S�d�T4v"����NC�A���I�Z�N���?���0>����9�k��W?}}�v������(�Q!^��������+X�������)��2�M�����6�=��.�2�����Li
����i�>����%��t�D���OZ�Dye�����V1�!�y1�
a����n;_K�� ��p:�Iqk":0�_gvm�TY�N���#����2���0,?��������0.7X��x�����^��8�^�_�
�v:,�U��US��H����fA
H�mC���Zm�}+��������\����zQ�*� )�U��R��kU�P�u��
����^t�XP���%H�u�4K��"^��=��L[����Y}N����|#wx?�{��P�A�!$ ��7�u��K�x$�zr�R!qW]]�mt$a{��B� Q77�������D�����D��nm���3���v��}ss���7����Y�2F��)c(Zo@��v�?@��E�`���s6w��+0o*�gh����nD���;g��5b�gw	�{�IRr��L2(���#�����,2(�|�22�X� �!2���QQ~�:�I�����h���	��Oto��W�A��fY�?��A$��C��Jm>_�6�#���,wC9 Hl��+=���2pY�7qKM��;.��m�FG��m���5�:.���(6�)pL�$������1��)�3�|.�4D��W���@6r�{���r�P7�|_�C�NDm	���g�:n��s1t�kq��� 6r�W�/~2L0�sQ�IOMo����n&�c����$�/+{;����������������u�������>�-'+�cl~E�hg�
"�`]�Q�`a
mt�vP�iR<����:�~�Zo���O��UC� b��XWA~������lc�XT��@U b�������
�F���\�����SpFE B���{�pN�c��T~���@��C|��%d|�W%_�&R�*��$lN��(*Sv��J!'X�;G]KmV���%5��i�d3$�p�E@9&�����t
xSQ��EPUw��:��� �c�KDG'�����\B�v.4��+����Q���7���sS�;3t���L �&p���V5�,����5�G{@�0���Q{����K����a�r�������`�,�T�v�p�=?M�O�g�� �i��4$7���&�d����d��4zV���,���u
n���y����c� �sk)N~V$:K�Ga������������8f�
s�����8����e�G�����c#�ccy�g���0�Z��_bs,W����lg)�M��u�2�d�Lg)�M��s���d�,g)�M��q�2�d�g+�u�?e{��`5b�gQ=(
��sP8UI�|��o~�b���O]/TN[����e�Z����h5��������y��%��o,V��R$���m��0m��B��0��)u�G�V��*O�,p�=�v���F���j�)�W��pMz9����>',�w~&�=��Wv��_�|	��,��q2���F<t/���+�Cn���2{9��|:�Y����Fo|�+t���%����O�,���<����4����Q����BsG}�������+��������i0uxs.�-+yrj\���,*n��B��TNf
G+�7ce����)��N��OaM|y5p������3�9�A�����%�o����5g�sH��t;'+E�Di�GI��VV�q�)�w+��������b�A$����vw���}�U�~Ws��
�����W�#���*?-o28�����r�����Icz����������[�o;[
����v���DF7�z���-�e�O��
?�<o�@��ew9�����O���4�:����m>��n��e�x�w�������osR��sx���+���� �N���?;�$���+r��GA�w���b���>�w��hf��U^.sN����E�����,V����P*[������0A�����Y\"����e���BX���~����^�.kP��7J�1���VM�a
������T��V�]{��a�n�<��^���5}n�G�2�����
������7��U&��|c����+��� I��|l���CU2�njK��4O���|_�B]o��"�k���L�>W��������4�z���U��i��z��\-.b��tT]v.6����~�)?,�^����,���8��)m�&����E�m�\���.=^�*D���F���8GGi��y�H�Bb���/�H#�.*��H�xQKY���\�����3�pUk�H�+��H��4r���-+�1+*����A������4����j\�@���Os�c�+�|�����{q��x���r��r>���4������
����W�g��t�>��� ���^����
|��f���v�[�=>��9�����A9>�6G�.u����/i��n�~V6K�����GP�����zB1q�����R�P�#(��h�� 
�[����t������c4
����&Z�g��X�u��
<��^h3�����h]/������?�O6��P��
oq��,�N�l�.�b��n���[����m$�V7�n_>��K��#�|�m���(J��r��2�>��P��
�������S-`.��x�
�;�	��V�B�jK��!���+,�H�������m�p�u�Ip|��WCk��%
����H�+mAX�S��>���,a����&0({H6�;${@6h@fg86p8fq06h0fg(6x(fy 6n fo6hfg6dfc6bfa6ffg�5h�ei�5b�ee�5b�ee�U��������!��db~
v�j�x�R�%�^e�_��c��"����5��Z��[M�v���fi�k]�)�.�����b]M���tFkl��@�d<%�E�d����������.e{kC����v�^i����;Z(p��FkG+����6L;Z�����A�i���&�E5+Rx���b����j�������[��o��=M����[��ok�-MmK�jIE�����s���'Yw��l������J�����fL�M���E~h��-?n��!q���J��[�)��69����,�`J�MP*x��1=���?����b��.H��QTp��6���������hd�qes9�qT��w�g�������#W���l-�L�FS�Eer8�5��WWd�g��*��0�(U1��ie	v\`�
������c��2U���'c����W�&�_�])�;�Z�Z���Y��������Qu�8>����$/�����,��y�X�Gq���uK�=��s����F<[,�m�����.��M@�#�|q<v�����:��l�S]�i&f��wS��~"�@nw�HkX$W.����E��&~7"�bmIt��p_�$����3���>8�Uv7.)qemDT�v0W��c���a��h��[��X�<Y#�V��'�^�LM�W�Pe���`6Qf}WU|�r�/������S3�M��6D��P�U��#��5�w/��u"�70�%�"����H�����Hd���ZH�7���^��P��|W�,8�)�:���S7���/C�d��d6A�����d�����_�HoTNG��im\tO���ZH����C�VZ>�����z��k@��@����ai�u��pIn��dL/
�����w	��������	�%:��� \[`���|������c/���-��(�Q���!�kt$�7�Q����[�S��s���k2�"l.����8��Stx�?�Q���V�/��Mr�wM�^��	
XLA����;�f�A�6����p���3���kB���"`���z3��+4�e����b��T�t��/In��d�����y`�:_�KM�Y�=k�C�G���K�'�;��/A6�v^L��VZ�������G�����������N��v�jo\���.�s�UADHz�+����5������s�v�L��!�x�U��~���L(�
�_�����������q����E�����O�hS�B����uK8����b�/Nq^��9�����H������[Y����Nh��o��YX�:d�g�����Y�/Mv�H����������=�������*�����}O� �J��R����7:Ty��W%w�t�L5<M�����w���������\����|���%�������tN���=���=��:?���c,r���'KsDA�s��u�s7u��q�2��H{�28f{�Sf6��$���C�v���*��Trk\�(b	��@�W�A���x�������?h��I�fQ�zp���L9?�;�h����+����@m4������[uH�.�\'��M[�P�0^[hd��;���c8A@m�`�,5��~��F}Gt����yV��}d6���=T*�=���I�}���#���Q��,�Mo5{�28f{��SfSci�7��D�!!��{0��i��C����1�z�x��tk��Oo������`�E9��UI�i��}i�F���
�����XL�]�h5mclo�{mS���6��s[���Fu����So���!���Q-�sz����Z�`��mU�7���(�:�YL�2�_�����MD?2v����wv�=����jo��	�D�n�P:�����\�E�"3��(�W����mT�/�G�J�����.���=�<�	�c������<dx�s|BDo;����utY�$����G�;/����@�y��At�vV4�~)g��8�,@���H)���s�-��F���b���phV�y�
��?�u
������q�y�m��\��v�F����35.��Wl�W�H��\�R�������\�.�9�~u7��R��<{���7�����_��?	#��?���tb������������8}?�|�>���&l&��r(�-����.�����;��k�a�n��(�I|&�[�Twct��� }�[�^����2����9�#�����C�y�7�m�e���y�-�g�<�g��l-~�H��=~n�_#����(���]�����`���/j���?|���Gv8?��[�Q��O�-o ��_Mx���K���h4�A���{���Wg�E!f�>�d-����$||~:A�\R���z��|�7Z����Kv7��z���;������!E��,s."��_$�Y����+B}�*/`�r�T�B��Rh��*�7��'\%���or7���[����<,4������9�ec�?D9���,(�������\��y��ky�o�WE�;��I|���r�K�6d3��u7H�A9���6=���z���6o���7�1k����_k�*"R|����s������������7��f��I���:�ys����o�(�n8w�5�����Bu���_�h�5������_	�"^�����j�h��d�KS�����l}���h��u�:��m���}h<�D��x�O&��/\�B�=����K�m���G��]�b���1�o=�~��h���[��71�M�c���xd�E�b�E~~�^����~�������rH�U6D��Ic�p�+��g,�	�/�]����
�L'���������������>;�X���
�m�R�(
j]�Iw�����X�K����!��s�~/^��N�?��1mK�7��P���$I��JH������������O'?�w���R2�_B�{��$��6�fu�r%���J"�{����+������X�R]*�]���W�Yq=�Q~�sdY&r����E�r��0?��!DQOx��1��:�G0jH����L�W��]�'�}��[z�4@_X��K>�o��w�l��{�h�R�,D�������o1��������:��#��?������S����flw�nvNjLxd��s1K|T���(�.�^�d���A�|����������g��g���m���/??�����+�g+�{������{2���������Y�]'k7�=]�,h#+��*�["����C�L���7�����7���2U5�@\�2g�A�\�����^��R�)�m{E�Cw3�@�,��d�;_����������{Y���d�%���r:}��#�}�x�-P�0
fw����lb��F�X�����F���2��&���B�u��8��f"�"��T��� x}�h�B�|\~�/i=�����5����k8��y��!weU�?�<�-h���r�����FZ�jCc���Zb�(
�G���b�}�ak: CoO��7��o��6kk���9rp����C9�n���/Z��5�c�U����)��z�>*To:�!CnL��E��I�S�ig�!����jJZ/e��������H�$��a�g|DoU��z+z��������������y��J�n�b�/�0��
��q��
M^W�����qs���@#��5�es�S���O��T�����g*������s�Fd�*c��	P�j�j����g�,���/�h���xY�G�;<��hm#S����@�V���@�&b���|��!�
�0N�w7�UN��?�]���RF����B��?=���f�[rX���-5�o
k��C�^��V7�M�Z�|�F�����\P�tI���6bJ�����E������~#��.�/N��W����qu��C~-��U�J�a4�
H�����e����
��eFi�R_Q������6^���4�i�@E"3y����������g������\��O����J����(79�m��k��$Z9������q���@���.���#6oVrv����&]v�$�?��?�������I�(���,�����^#���#��]�`����0.7�?D�k���{$9�����h�>:���d[0��������M��%n�J<(���#J��=�d����1riuN�aD�Q.l���_��`��^�nSr(Z�"E��f�Wd�s�Y��'�!���x/d�U�A��*���e}pt�E��W"K>�;9Z�.a�8WC<���-��K��k�����z��B^��Q���������t*7������8��s�m��%u�M"���`��U5u������K58�y'SY������4	J���U��r��"9���!PG��,u���[�-:�7���%+�a���oz��<�����E��)��,���[�-;��,Me7�]l���mP�A(=��v��/	s���'?�2�����$>�[WxT����
�:	�3�����$��]��K9��OK�
�2e��A=�N������wQq&��m�o����� E�����.qHi�x(R�T�����t����8-2�-�7w]R�
�����8�4j=����+�7{����tXQd�����vh�.��tu�������=\�������iWl���:pb��vm�qY���4r����$n�9�oi����1'��d�YP��T�����y������
�������Lv�Du.a��8�'�r���!wzf;������o���N�����{���=|A�p{sy������>x��F)�r�����7_�I_����@�g��48���m������E��[QFP��'Q����bqT=;�#e����c W�59B�2�T���K���|y)������u����F{@�,m�G��.h}�{S����%U��_.�L&,l�������tFFW�sQCr���}]��7KKwV�rBe���s����$�������O�&1�z�
�^�7u�_�%rJ�������E,�q�\�+	�wJag)��F��Mx�H����V]��8�����Q��b{���
��n��S<$�iU_6��O�_$�?e$�2���8�����olAe/����mSm,����^$���k�;��|�/t]P?��:��zX�(��*��HU���k6�\N?�����B����U�X|l=�!DL�P\�R)�k������%{��6�L3v2��4oe�����X��jFD�ZQ���7��6����m5�{�����S��/9;��������vG����Y�O���9�����=�fug�^���9��[��B�t��K�����i�k���)����Z�%N91I�>�~x���B�,��� bbsN��V�����Y\�#o-H���8qq��!.~��_�zV�o����3�3�����������s���O�/���!Q�$��Y�#z��K������d1>�=:����#i��HB^=���6�b?{� YU���z��?���4��o�{���"$�_����y[]V��E���o|���o�K��� ���K�`�$����uk�`�P���.6_���������^�u�?�����ct�Cy������K��o�����1�4���(?&����O����?__
l�!�����[ypg��S���Q�<v��(���Ox���\��^�-�o�����V��z����R2�)Xf��r*^��C*�%X����H����CF
]L�8�|U��G�p���,���[��|�-�����[{�=��|���.�uZ��'	���=�IG{D^�����a�;o��u����c��_N�L����Grn���I�&��;D��S<�xCF�R��r�L<������,�pA�[x������u$�`���D!��XK�_\/#hL"I�e�:�z+x�"�G��]�&fb���'��v���{����Eho1����KH�p����<�F���C]���?�K]�������D�~,�Jy7�f��r]6��v�G�~��s�,�V|���2����|�"�(�K��M]�~�(P'D�!�<�K!{]J�����/�MS���?�oB�.�
fix_proc_data.pltext/x-perl; charset=us-ascii; name=fix_proc_data.plDownload
#68Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Tom Lane (#67)
Re: prokind column (was Re: [HACKERS] SQL procedures)

On 2/28/18 15:45, Tom Lane wrote:

I have reviewed this patch and attach an updated version below.
I've rebased it up to today, fixed a few minor errors, and adopted
most of Michael's suggestions. Also, since I remain desperately
unhappy with putting zeroes into prorettype, I changed it to not
do that ;-) ... now procedures have VOIDOID as their prorettype,
and it will be substantially less painful to allow them to return
some other scalar result in future, should we wish to. I believe
I've found all the places that were relying on prorettype == 0 as
a substitute for prokind == 'p'.

I have just posted "INOUT parameters in procedures", which contains some
of those same changes. So I think we're on the same page. I will work
on consolidating this.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#69Michael Paquier
michael@paquier.xyz
In reply to: Peter Eisentraut (#68)
Re: prokind column (was Re: [HACKERS] SQL procedures)

On Wed, Feb 28, 2018 at 05:37:11PM -0500, Peter Eisentraut wrote:

On 2/28/18 15:45, Tom Lane wrote:

I have reviewed this patch and attach an updated version below.
I've rebased it up to today, fixed a few minor errors, and adopted
most of Michael's suggestions. Also, since I remain desperately
unhappy with putting zeroes into prorettype, I changed it to not
do that ;-) ... now procedures have VOIDOID as their prorettype,
and it will be substantially less painful to allow them to return
some other scalar result in future, should we wish to. I believe
I've found all the places that were relying on prorettype == 0 as
a substitute for prokind == 'p'.

I have just posted "INOUT parameters in procedures", which contains some
of those same changes. So I think we're on the same page. I will work
on consolidating this.

Thanks Peter.

I have read the patch set that Tom has posted here and hunted for other
inconsistencies. It seems to me that you have spotted everything.

The changes in ProcedureCreate() are nice.

I was wondering as well if it would be worth checking the contents of
pg_proc with prorettype = 0 in the regression tests to always make sure
that this never happens... Until I saw that opr_sanity.sql was actually
doing already that so procedures actually are breaking that check on
HEAD.
--
Michael

#70Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Peter Eisentraut (#68)
Re: prokind column (was Re: [HACKERS] SQL procedures)

On 2/28/18 17:37, Peter Eisentraut wrote:

On 2/28/18 15:45, Tom Lane wrote:

I have reviewed this patch and attach an updated version below.
I've rebased it up to today, fixed a few minor errors, and adopted
most of Michael's suggestions. Also, since I remain desperately
unhappy with putting zeroes into prorettype, I changed it to not
do that ;-) ... now procedures have VOIDOID as their prorettype,
and it will be substantially less painful to allow them to return
some other scalar result in future, should we wish to. I believe
I've found all the places that were relying on prorettype == 0 as
a substitute for prokind == 'p'.

I have just posted "INOUT parameters in procedures", which contains some
of those same changes. So I think we're on the same page. I will work
on consolidating this.

Committed this so far. More to come on touching this up.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services