Thread

  1. [PATCH 1/4] Add REPACK command

    Antonin Houska <ah@cybertec.at> — 2025-12-04T17:20:07Z

    REPACK absorbs the functionality of VACUUM FULL and CLUSTER in a single
    command.  Because this functionality is completely different from
    regular VACUUM, having it separate from VACUUM makes it easier for users
    to understand; as for CLUSTER, the term is heavily overloaded in the
    IT world and even in Postgres itself, so it's good that we can avoid it.
    
    This also adds pg_repackdb, a new utility that can invoke the new
    commands.  This is heavily based on vacuumdb.
    
    Author: Antonin Houska <ah@cybertec.at>
    Co-authored-by: =C3=81lvaro Herrera <alvherre@kurilemu.de>
    Reviewed-by: Mihail Nikalayeu <mihailnikalayeu@gmail.com>
    Reviewed-by: Robert Treat <rob@xzilla.net>
    Reviewed-by: Euler Taveira <euler@eulerto.com>
    Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
    Reviewed-by: Junwang Zhao <zhjwpku@gmail.com>
    Reviewed-by: jian he <jian.universality@gmail.com>
    Discussion: https://postgr.es/m/82651.1720540558@antos
    Discussion: https://postgr.es/m/202507262156.sb455angijk6@alvherre.pgsql
    ---
     doc/src/sgml/monitoring.sgml             | 223 +++++-
     doc/src/sgml/ref/allfiles.sgml           |   2 +
     doc/src/sgml/ref/cluster.sgml            |  97 +--
     doc/src/sgml/ref/clusterdb.sgml          |   5 +
     doc/src/sgml/ref/pg_repackdb.sgml        | 488 +++++++++++++
     doc/src/sgml/ref/repack.sgml             | 328 +++++++++
     doc/src/sgml/ref/vacuum.sgml             |  33 +-
     doc/src/sgml/reference.sgml              |   2 +
     src/backend/access/heap/heapam_handler.c |  32 +-
     src/backend/catalog/index.c              |   2 +-
     src/backend/catalog/system_views.sql     |  29 +-
     src/backend/commands/cluster.c           | 851 +++++++++++++++--------
     src/backend/commands/vacuum.c            |   6 +-
     src/backend/parser/gram.y                |  86 ++-
     src/backend/tcop/utility.c               |  20 +-
     src/backend/utils/adt/pgstatfuncs.c      |   2 +
     src/bin/psql/tab-complete.in.c           |  42 +-
     src/bin/scripts/Makefile                 |   4 +-
     src/bin/scripts/meson.build              |   2 +
     src/bin/scripts/pg_repackdb.c            | 242 +++++++
     src/bin/scripts/t/103_repackdb.pl        |  47 ++
     src/bin/scripts/vacuuming.c              | 114 ++-
     src/bin/scripts/vacuuming.h              |   3 +
     src/include/commands/cluster.h           |   8 +-
     src/include/commands/progress.h          |  50 +-
     src/include/nodes/parsenodes.h           |  35 +-
     src/include/parser/kwlist.h              |   1 +
     src/include/tcop/cmdtaglist.h            |   1 +
     src/include/utils/backend_progress.h     |   1 +
     src/test/regress/expected/cluster.out    | 134 +++-
     src/test/regress/expected/rules.out      |  72 +-
     src/test/regress/sql/cluster.sql         |  70 +-
     src/tools/pgindent/typedefs.list         |   2 +
     33 files changed, 2490 insertions(+), 544 deletions(-)
     create mode 100644 doc/src/sgml/ref/pg_repackdb.sgml
     create mode 100644 doc/src/sgml/ref/repack.sgml
     create mode 100644 src/bin/scripts/pg_repackdb.c
     create mode 100644 src/bin/scripts/t/103_repackdb.pl
    
    diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
    index 039d73691be..b8da77b4d89 100644
    --- a/doc/src/sgml/monitoring.sgml
    +++ b/doc/src/sgml/monitoring.sgml
    @@ -405,6 +405,14 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   =
    11:34   0:00 postgres: ser
           </entry>
          </row>
    =20
    +     <row>
    +      <entry><structname>pg_stat_progress_repack</structname><indexterm><p=
    rimary>pg_stat_progress_repack</primary></indexterm></entry>
    +      <entry>One row for each backend running
    +       <command>REPACK</command>, showing current progress.  See
    +       <xref linkend=3D"repack-progress-reporting"/>.
    +      </entry>
    +     </row>
    +
          <row>
           <entry><structname>pg_stat_progress_basebackup</structname><indexter=
    m><primary>pg_stat_progress_basebackup</primary></indexterm></entry>
           <entry>One row for each WAL sender process streaming a base backup,
    @@ -5609,7 +5617,8 @@ FROM pg_stat_get_backend_idset() AS backendid;
        certain commands during command execution.  Currently, the only commands
        which support progress reporting are <command>ANALYZE</command>,
        <command>CLUSTER</command>,
    -   <command>CREATE INDEX</command>, <command>VACUUM</command>,
    +   <command>CREATE INDEX</command>, <command>REPACK</command>,
    +   <command>VACUUM</command>,
        <command>COPY</command>,
        and <xref linkend=3D"protocol-replication-base-backup"/> (i.e., replica=
    tion
        command that <xref linkend=3D"app-pgbasebackup"/> issues to take
    @@ -6068,6 +6077,218 @@ FROM pg_stat_get_backend_idset() AS backendid;
       </table>
      </sect2>
    =20
    + <sect2 id=3D"repack-progress-reporting">
    +  <title>REPACK Progress Reporting</title>
    +
    +  <indexterm>
    +   <primary>pg_stat_progress_repack</primary>
    +  </indexterm>
    +
    +  <para>
    +   Whenever <command>REPACK</command> is running,
    +   the <structname>pg_stat_progress_repack</structname> view will contain a
    +   row for each backend that is currently running the command.  The tables
    +   below describe the information that will be reported and provide
    +   information about how to interpret it.
    +  </para>
    +
    +  <table id=3D"pg-stat-progress-repack-view" xreflabel=3D"pg_stat_progress=
    _repack">
    +   <title><structname>pg_stat_progress_repack</structname> View</title>
    +   <tgroup cols=3D"1">
    +    <thead>
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       Column Type
    +      </para>
    +      <para>
    +       Description
    +      </para></entry>
    +     </row>
    +    </thead>
    +
    +    <tbody>
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>pid</structfield> <type>integer</type>
    +      </para>
    +      <para>
    +       Process ID of backend.
    +      </para></entry>
    +     </row>
    +
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>datid</structfield> <type>oid</type>
    +      </para>
    +      <para>
    +       OID of the database to which this backend is connected.
    +      </para></entry>
    +     </row>
    +
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>datname</structfield> <type>name</type>
    +      </para>
    +      <para>
    +       Name of the database to which this backend is connected.
    +      </para></entry>
    +     </row>
    +
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>relid</structfield> <type>oid</type>
    +      </para>
    +      <para>
    +       OID of the table being repacked.
    +      </para></entry>
    +     </row>
    +
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>phase</structfield> <type>text</type>
    +      </para>
    +      <para>
    +       Current processing phase. See <xref linkend=3D"repack-phases"/>.
    +      </para></entry>
    +     </row>
    +
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>repack_index_relid</structfield> <type>oid</type>
    +      </para>
    +      <para>
    +       If the table is being scanned using an index, this is the OID of the
    +       index being used; otherwise, it is zero.
    +      </para></entry>
    +     </row>
    +
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>heap_tuples_scanned</structfield> <type>bigint</type>
    +      </para>
    +      <para>
    +       Number of heap tuples scanned.
    +       This counter only advances when the phase is
    +       <literal>seq scanning heap</literal>,
    +       <literal>index scanning heap</literal>
    +       or <literal>writing new heap</literal>.
    +      </para></entry>
    +     </row>
    +
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>heap_tuples_written</structfield> <type>bigint</type>
    +      </para>
    +      <para>
    +       Number of heap tuples written.
    +       This counter only advances when the phase is
    +       <literal>seq scanning heap</literal>,
    +       <literal>index scanning heap</literal>
    +       or <literal>writing new heap</literal>.
    +      </para></entry>
    +     </row>
    +
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>heap_blks_total</structfield> <type>bigint</type>
    +      </para>
    +      <para>
    +       Total number of heap blocks in the table.  This number is reported
    +       as of the beginning of <literal>seq scanning heap</literal>.
    +      </para></entry>
    +     </row>
    +
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>heap_blks_scanned</structfield> <type>bigint</type>
    +      </para>
    +      <para>
    +       Number of heap blocks scanned.  This counter only advances when the
    +       phase is <literal>seq scanning heap</literal>.
    +      </para></entry>
    +     </row>
    +
    +     <row>
    +      <entry role=3D"catalog_table_entry"><para role=3D"column_definition">
    +       <structfield>index_rebuild_count</structfield> <type>bigint</type>
    +      </para>
    +      <para>
    +       Number of indexes rebuilt.  This counter only advances when the pha=
    se
    +       is <literal>rebuilding index</literal>.
    +      </para></entry>
    +     </row>
    +    </tbody>
    +   </tgroup>
    +  </table>
    +
    +  <table id=3D"repack-phases">
    +   <title>REPACK Phases</title>
    +   <tgroup cols=3D"2">
    +    <colspec colname=3D"col1" colwidth=3D"1*"/>
    +    <colspec colname=3D"col2" colwidth=3D"2*"/>
    +    <thead>
    +    <row>
    +      <entry>Phase</entry>
    +      <entry>Description</entry>
    +     </row>
    +    </thead>
    +
    +   <tbody>
    +    <row>
    +     <entry><literal>initializing</literal></entry>
    +     <entry>
    +       The command is preparing to begin scanning the heap.  This phase is
    +       expected to be very brief.
    +     </entry>
    +    </row>
    +    <row>
    +     <entry><literal>seq scanning heap</literal></entry>
    +     <entry>
    +       The command is currently scanning the table using a sequential scan.
    +     </entry>
    +    </row>
    +    <row>
    +     <entry><literal>index scanning heap</literal></entry>
    +     <entry>
    +       <command>REPACK</command> is currently scanning the table using an =
    index scan.
    +     </entry>
    +    </row>
    +    <row>
    +     <entry><literal>sorting tuples</literal></entry>
    +     <entry>
    +       <command>REPACK</command> is currently sorting tuples.
    +     </entry>
    +    </row>
    +    <row>
    +     <entry><literal>writing new heap</literal></entry>
    +     <entry>
    +       <command>REPACK</command> is currently writing the new heap.
    +     </entry>
    +    </row>
    +    <row>
    +     <entry><literal>swapping relation files</literal></entry>
    +     <entry>
    +       The command is currently swapping newly-built files into place.
    +     </entry>
    +    </row>
    +    <row>
    +     <entry><literal>rebuilding index</literal></entry>
    +     <entry>
    +       The command is currently rebuilding an index.
    +     </entry>
    +    </row>
    +    <row>
    +     <entry><literal>performing final cleanup</literal></entry>
    +     <entry>
    +       The command is performing final cleanup.  When this phase is
    +       completed, <command>REPACK</command> will end.
    +     </entry>
    +    </row>
    +   </tbody>
    +   </tgroup>
    +  </table>
    + </sect2>
    +
      <sect2 id=3D"copy-progress-reporting">
       <title>COPY Progress Reporting</title>
    =20
    diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
    index e167406c744..5df944d13ca 100644
    --- a/doc/src/sgml/ref/allfiles.sgml
    +++ b/doc/src/sgml/ref/allfiles.sgml
    @@ -167,6 +167,7 @@ Complete list of usable sgml source files in this direc=
    tory.
     <!ENTITY refreshMaterializedView SYSTEM "refresh_materialized_view.sgml">
     <!ENTITY reindex            SYSTEM "reindex.sgml">
     <!ENTITY releaseSavepoint   SYSTEM "release_savepoint.sgml">
    +<!ENTITY repack             SYSTEM "repack.sgml">
     <!ENTITY reset              SYSTEM "reset.sgml">
     <!ENTITY revoke             SYSTEM "revoke.sgml">
     <!ENTITY rollback           SYSTEM "rollback.sgml">
    @@ -213,6 +214,7 @@ Complete list of usable sgml source files in this direc=
    tory.
     <!ENTITY pgIsready          SYSTEM "pg_isready.sgml">
     <!ENTITY pgReceivewal       SYSTEM "pg_receivewal.sgml">
     <!ENTITY pgRecvlogical      SYSTEM "pg_recvlogical.sgml">
    +<!ENTITY pgRepackdb         SYSTEM "pg_repackdb.sgml">
     <!ENTITY pgResetwal         SYSTEM "pg_resetwal.sgml">
     <!ENTITY pgRestore          SYSTEM "pg_restore.sgml">
     <!ENTITY pgRewind           SYSTEM "pg_rewind.sgml">
    diff --git a/doc/src/sgml/ref/cluster.sgml b/doc/src/sgml/ref/cluster.sgml
    index 0b47460080b..2cda711bc9f 100644
    --- a/doc/src/sgml/ref/cluster.sgml
    +++ b/doc/src/sgml/ref/cluster.sgml
    @@ -33,51 +33,13 @@ CLUSTER [ ( <replaceable class=3D"parameter">option</re=
    placeable> [, ...] ) ] [ <r
       <title>Description</title>
    =20
       <para>
    -   <command>CLUSTER</command> instructs <productname>PostgreSQL</productna=
    me>
    -   to cluster the table specified
    -   by <replaceable class=3D"parameter">table_name</replaceable>
    -   based on the index specified by
    -   <replaceable class=3D"parameter">index_name</replaceable>. The index mu=
    st
    -   already have been defined on
    -   <replaceable class=3D"parameter">table_name</replaceable>.
    +   The <command>CLUSTER</command> command is equivalent to
    +   <xref linkend=3D"sql-repack"/> with an <literal>USING INDEX</literal>
    +   clause.  See there for more details.
       </para>
    =20
    -  <para>
    -   When a table is clustered, it is physically reordered
    -   based on the index information. Clustering is a one-time operation:
    -   when the table is subsequently updated, the changes are
    -   not clustered.  That is, no attempt is made to store new or
    -   updated rows according to their index order.  (If one wishes, one can
    -   periodically recluster by issuing the command again.  Also, setting
    -   the table's <literal>fillfactor</literal> storage parameter to less than
    -   100% can aid in preserving cluster ordering during updates, since updat=
    ed
    -   rows are kept on the same page if enough space is available there.)
    -  </para>
    -
    -  <para>
    -   When a table is clustered, <productname>PostgreSQL</productname>
    -   remembers which index it was clustered by.  The form
    -   <command>CLUSTER <replaceable class=3D"parameter">table_name</replaceab=
    le></command>
    -   reclusters the table using the same index as before.  You can also
    -   use the <literal>CLUSTER</literal> or <literal>SET WITHOUT CLUSTER</lit=
    eral>
    -   forms of <link linkend=3D"sql-altertable"><command>ALTER TABLE</command=
    ></link> to set the index to be used for
    -   future cluster operations, or to clear any previous setting.
    -  </para>
    -
    -  <para>
    -   <command>CLUSTER</command> without a
    -   <replaceable class=3D"parameter">table_name</replaceable> reclusters al=
    l the
    -   previously-clustered tables in the current database that the calling us=
    er
    -   has privileges for.  This form of <command>CLUSTER</command> cannot be
    -   executed inside a transaction block.
    -  </para>
    +<!-- Do we need to describe exactly which options map to what?  They seem =
    obvious to me. -->
    =20
    -  <para>
    -   When a table is being clustered, an <literal>ACCESS
    -   EXCLUSIVE</literal> lock is acquired on it. This prevents any other
    -   database operations (both reads and writes) from operating on the
    -   table until the <command>CLUSTER</command> is finished.
    -  </para>
      </refsect1>
    =20
      <refsect1>
    @@ -136,63 +98,12 @@ CLUSTER [ ( <replaceable class=3D"parameter">option</r=
    eplaceable> [, ...] ) ] [ <r
         on the table.
        </para>
    =20
    -   <para>
    -    In cases where you are accessing single rows randomly
    -    within a table, the actual order of the data in the
    -    table is unimportant. However, if you tend to access some
    -    data more than others, and there is an index that groups
    -    them together, you will benefit from using <command>CLUSTER</command>.
    -    If you are requesting a range of indexed values from a table, or a
    -    single indexed value that has multiple rows that match,
    -    <command>CLUSTER</command> will help because once the index identifies=
     the
    -    table page for the first row that matches, all other rows
    -    that match are probably already on the same table page,
    -    and so you save disk accesses and speed up the query.
    -   </para>
    -
    -   <para>
    -    <command>CLUSTER</command> can re-sort the table using either an index=
     scan
    -    on the specified index, or (if the index is a b-tree) a sequential
    -    scan followed by sorting.  It will attempt to choose the method that
    -    will be faster, based on planner cost parameters and available statist=
    ical
    -    information.
    -   </para>
    -
        <para>
         While <command>CLUSTER</command> is running, the <xref
         linkend=3D"guc-search-path"/> is temporarily changed to <literal>pg_ca=
    talog,
         pg_temp</literal>.
        </para>
    =20
    -   <para>
    -    When an index scan is used, a temporary copy of the table is created t=
    hat
    -    contains the table data in the index order.  Temporary copies of each
    -    index on the table are created as well.  Therefore, you need free spac=
    e on
    -    disk at least equal to the sum of the table size and the index sizes.
    -   </para>
    -
    -   <para>
    -    When a sequential scan and sort is used, a temporary sort file is
    -    also created, so that the peak temporary space requirement is as much
    -    as double the table size, plus the index sizes.  This method is often
    -    faster than the index scan method, but if the disk space requirement is
    -    intolerable, you can disable this choice by temporarily setting <xref
    -    linkend=3D"guc-enable-sort"/> to <literal>off</literal>.
    -   </para>
    -
    -   <para>
    -    It is advisable to set <xref linkend=3D"guc-maintenance-work-mem"/> to
    -    a reasonably large value (but not more than the amount of RAM you can
    -    dedicate to the <command>CLUSTER</command> operation) before clusterin=
    g.
    -   </para>
    -
    -   <para>
    -    Because the planner records statistics about the ordering of
    -    tables, it is advisable to run <link linkend=3D"sql-analyze"><command>=
    ANALYZE</command></link>
    -    on the newly clustered table.
    -    Otherwise, the planner might make poor choices of query plans.
    -   </para>
    -
        <para>
         Because <command>CLUSTER</command> remembers which indexes are cluster=
    ed,
         one can cluster the tables one wants clustered manually the first time,
    diff --git a/doc/src/sgml/ref/clusterdb.sgml b/doc/src/sgml/ref/clusterdb.s=
    gml
    index 0d2051bf6f1..546c1289c31 100644
    --- a/doc/src/sgml/ref/clusterdb.sgml
    +++ b/doc/src/sgml/ref/clusterdb.sgml
    @@ -64,6 +64,11 @@ PostgreSQL documentation
        this utility and via other methods for accessing the server.
       </para>
    =20
    +  <para>
    +   <application>clusterdb</application> has been superceded by
    +   <application>pg_repackdb</application>.
    +  </para>
    +
      </refsect1>
    =20
    =20
    diff --git a/doc/src/sgml/ref/pg_repackdb.sgml b/doc/src/sgml/ref/pg_repack=
    db.sgml
    new file mode 100644
    index 00000000000..b313b54ab63
    --- /dev/null
    +++ b/doc/src/sgml/ref/pg_repackdb.sgml
    @@ -0,0 +1,488 @@
    +<!--
    +doc/src/sgml/ref/pg_repackdb.sgml
    +PostgreSQL documentation
    +-->
    +
    +<refentry id=3D"app-pgrepackdb">
    + <indexterm zone=3D"app-pgrepackdb">
    +  <primary>pg_repackdb</primary>
    + </indexterm>
    +
    + <refmeta>
    +  <refentrytitle><application>pg_repackdb</application></refentrytitle>
    +  <manvolnum>1</manvolnum>
    +  <refmiscinfo>Application</refmiscinfo>
    + </refmeta>
    +
    + <refnamediv>
    +  <refname>pg_repackdb</refname>
    +  <refpurpose>repack and analyze a <productname>PostgreSQL</productname>
    +  database</refpurpose>
    + </refnamediv>
    +
    + <refsynopsisdiv>
    +  <cmdsynopsis>
    +   <command>pg_repackdb</command>
    +   <arg rep=3D"repeat"><replaceable>connection-option</replaceable></arg>
    +   <arg rep=3D"repeat"><replaceable>option</replaceable></arg>
    +
    +   <arg choice=3D"plain" rep=3D"repeat">
    +    <arg choice=3D"opt">
    +     <group choice=3D"plain">
    +      <arg choice=3D"plain"><option>-t</option></arg>
    +      <arg choice=3D"plain"><option>--table</option></arg>
    +     </group>
    +     <replaceable>table</replaceable>
    +     <arg choice=3D"opt">( <replaceable class=3D"parameter">column</replac=
    eable> [,...] )</arg>
    +    </arg>
    +   </arg>
    +
    +   <arg choice=3D"opt">
    +    <group choice=3D"plain">
    +     <arg choice=3D"plain"><replaceable>dbname</replaceable></arg>
    +     <arg choice=3D"plain"><option>-a</option></arg>
    +     <arg choice=3D"plain"><option>--all</option></arg>
    +    </group>
    +   </arg>
    +  </cmdsynopsis>
    +
    +  <cmdsynopsis>
    +   <command>pg_repackdb</command>
    +   <arg rep=3D"repeat"><replaceable>connection-option</replaceable></arg>
    +   <arg rep=3D"repeat"><replaceable>option</replaceable></arg>
    +
    +   <arg choice=3D"plain" rep=3D"repeat">
    +    <arg choice=3D"opt">
    +     <group choice=3D"plain">
    +      <arg choice=3D"plain"><option>-n</option></arg>
    +      <arg choice=3D"plain"><option>--schema</option></arg>
    +     </group>
    +     <replaceable>schema</replaceable>
    +    </arg>
    +   </arg>
    +
    +   <arg choice=3D"opt">
    +    <group choice=3D"plain">
    +     <arg choice=3D"plain"><replaceable>dbname</replaceable></arg>
    +     <arg choice=3D"plain"><option>-a</option></arg>
    +     <arg choice=3D"plain"><option>--all</option></arg>
    +    </group>
    +   </arg>
    +  </cmdsynopsis>
    +
    +  <cmdsynopsis>
    +   <command>pg_repackdb</command>
    +   <arg rep=3D"repeat"><replaceable>connection-option</replaceable></arg>
    +   <arg rep=3D"repeat"><replaceable>option</replaceable></arg>
    +
    +   <arg choice=3D"plain" rep=3D"repeat">
    +    <arg choice=3D"opt">
    +     <group choice=3D"plain">
    +      <arg choice=3D"plain"><option>-N</option></arg>
    +      <arg choice=3D"plain"><option>--exclude-schema</option></arg>
    +     </group>
    +     <replaceable>schema</replaceable>
    +    </arg>
    +   </arg>
    +
    +   <arg choice=3D"opt">
    +    <group choice=3D"plain">
    +     <arg choice=3D"plain"><replaceable>dbname</replaceable></arg>
    +     <arg choice=3D"plain"><option>-a</option></arg>
    +     <arg choice=3D"plain"><option>--all</option></arg>
    +    </group>
    +   </arg>
    +  </cmdsynopsis>
    + </refsynopsisdiv>
    +
    + <refsect1>
    +  <title>Description</title>
    +
    +  <para>
    +   <application>pg_repackdb</application> is a utility for repacking a
    +   <productname>PostgreSQL</productname> database.
    +   <application>pg_repackdb</application> will also generate internal
    +   statistics used by the <productname>PostgreSQL</productname> query
    +   optimizer.
    +  </para>
    +
    +  <para>
    +   <application>pg_repackdb</application> is a wrapper around the SQL
    +   command <link linkend=3D"sql-repack"><command>REPACK</command></link> T=
    here
    +   is no effective difference between repacking and analyzing databases via
    +   this utility and via other methods for accessing the server.
    +  </para>
    + </refsect1>
    +
    + <refsect1>
    +  <title>Options</title>
    +
    +   <para>
    +    <application>pg_repackdb</application> accepts the following command-l=
    ine arguments:
    +    <variablelist>
    +     <varlistentry>
    +      <term><option>-a</option></term>
    +      <term><option>--all</option></term>
    +      <listitem>
    +       <para>
    +        Repack all databases.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option><optional>-d</optional> <replaceable class=3D"paramete=
    r">dbname</replaceable></option></term>
    +      <term><option><optional>--dbname=3D</optional><replaceable class=3D"=
    parameter">dbname</replaceable></option></term>
    +      <listitem>
    +       <para>
    +        Specifies the name of the database to be repacked or analyzed,
    +        when <option>-a</option>/<option>--all</option> is not used.  If t=
    his
    +        is not specified, the database name is read from the environment
    +        variable <envar>PGDATABASE</envar>.  If that is not set, the user =
    name
    +        specified for the connection is used.
    +        The <replaceable>dbname</replaceable> can be
    +        a <link linkend=3D"libpq-connstring">connection string</link>.  If=
     so,
    +        connection string parameters will override any conflicting command
    +        line options.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-e</option></term>
    +      <term><option>--echo</option></term>
    +      <listitem>
    +       <para>
    +        Echo the commands that <application>pg_repackdb</application>
    +        generates and sends to the server.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>--index<optional>=3D<replaceable class=3D"parameter">i=
    ndex_name</replaceable></optional></option></term>
    +      <listitem>
    +       <para>
    +        Pass the <literal>USING INDEX</literal> clause to <literal>REPACK<=
    /literal>,
    +        and optionally the index name to specify.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-j <replaceable class=3D"parameter">njobs</replaceable=
    ></option></term>
    +      <term><option>--jobs=3D<replaceable class=3D"parameter">njobs</repla=
    ceable></option></term>
    +      <listitem>
    +       <para>
    +        Execute the repack or analyze commands in parallel by running
    +        <replaceable class=3D"parameter">njobs</replaceable>
    +        commands simultaneously.  This option may reduce the processing ti=
    me
    +        but it also increases the load on the database server.
    +       </para>
    +       <para>
    +        <application>pg_repackdb</application> will open
    +        <replaceable class=3D"parameter">njobs</replaceable> connections t=
    o the
    +        database, so make sure your <xref linkend=3D"guc-max-connections"/>
    +        setting is high enough to accommodate all connections.
    +       </para>
    +       <para>
    +        Note that using this mode might cause deadlock failures if certain
    +        system catalogs are processed in parallel.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-n <replaceable class=3D"parameter">schema</replaceabl=
    e></option></term>
    +      <term><option>--schema=3D<replaceable class=3D"parameter">schema</re=
    placeable></option></term>
    +      <listitem>
    +       <para>
    +        Repack or analyze all tables in
    +        <replaceable class=3D"parameter">schema</replaceable> only.  Multi=
    ple
    +        schemas can be repacked by writing multiple <option>-n</option>
    +        switches.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-N <replaceable class=3D"parameter">schema</replaceabl=
    e></option></term>
    +      <term><option>--exclude-schema=3D<replaceable class=3D"parameter">sc=
    hema</replaceable></option></term>
    +      <listitem>
    +       <para>
    +        Do not repack or analyze any tables in
    +        <replaceable class=3D"parameter">schema</replaceable>.  Multiple s=
    chemas
    +        can be excluded by writing multiple <option>-N</option> switches.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-q</option></term>
    +      <term><option>--quiet</option></term>
    +      <listitem>
    +       <para>
    +        Do not display progress messages.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-t <replaceable class=3D"parameter">table</replaceable=
    > [ (<replaceable class=3D"parameter">column</replaceable> [,...]) ]</optio=
    n></term>
    +      <term><option>--table=3D<replaceable class=3D"parameter">table</repl=
    aceable> [ (<replaceable class=3D"parameter">column</replaceable> [,...]) ]=
    </option></term>
    +      <listitem>
    +       <para>
    +        Repack or analyze <replaceable class=3D"parameter">table</replacea=
    ble>
    +        only.  Column names can be specified only in conjunction with
    +        the <option>--analyze</option> option.  Multiple tables can be
    +        repacked by writing multiple
    +        <option>-t</option> switches.
    +       </para>
    +       <tip>
    +        <para>
    +         If you specify columns, you probably have to escape the parenthes=
    es
    +         from the shell.  (See examples below.)
    +        </para>
    +       </tip>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-v</option></term>
    +      <term><option>--verbose</option></term>
    +      <listitem>
    +       <para>
    +        Print detailed information during processing.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +       <term><option>-V</option></term>
    +       <term><option>--version</option></term>
    +       <listitem>
    +       <para>
    +       Print the <application>pg_repackdb</application> version and exit.
    +       </para>
    +       </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-z</option></term>
    +      <term><option>--analyze</option></term>
    +      <listitem>
    +       <para>
    +        Also calculate statistics for use by the optimizer.  If a column n=
    ame
    +        list is given, only compute statistics for those columns.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +       <term><option>-?</option></term>
    +       <term><option>--help</option></term>
    +       <listitem>
    +       <para>
    +       Show help about <application>pg_repackdb</application> command line
    +       arguments, and exit.
    +       </para>
    +       </listitem>
    +     </varlistentry>
    +
    +    </variablelist>
    +   </para>
    +
    +   <para>
    +    <application>pg_repackdb</application> also accepts
    +    the following command-line arguments for connection parameters:
    +    <variablelist>
    +     <varlistentry>
    +      <term><option>-h <replaceable class=3D"parameter">host</replaceable>=
    </option></term>
    +      <term><option>--host=3D<replaceable class=3D"parameter">host</replac=
    eable></option></term>
    +      <listitem>
    +       <para>
    +        Specifies the host name of the machine on which the server
    +        is running.  If the value begins with a slash, it is used
    +        as the directory for the Unix domain socket.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-p <replaceable class=3D"parameter">port</replaceable>=
    </option></term>
    +      <term><option>--port=3D<replaceable class=3D"parameter">port</replac=
    eable></option></term>
    +      <listitem>
    +       <para>
    +        Specifies the TCP port or local Unix domain socket file
    +        extension on which the server
    +        is listening for connections.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-U <replaceable class=3D"parameter">username</replacea=
    ble></option></term>
    +      <term><option>--username=3D<replaceable class=3D"parameter">username=
    </replaceable></option></term>
    +      <listitem>
    +       <para>
    +        User name to connect as.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-w</option></term>
    +      <term><option>--no-password</option></term>
    +      <listitem>
    +       <para>
    +        Never issue a password prompt.  If the server requires
    +        password authentication and a password is not available by
    +        other means such as a <filename>.pgpass</filename> file, the
    +        connection attempt will fail.  This option can be useful in
    +        batch jobs and scripts where no user is present to enter a
    +        password.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>-W</option></term>
    +      <term><option>--password</option></term>
    +      <listitem>
    +       <para>
    +        Force <application>pg_repackdb</application> to prompt for a
    +        password before connecting to a database.
    +       </para>
    +
    +       <para>
    +        This option is never essential, since
    +        <application>pg_repackdb</application> will automatically prompt
    +        for a password if the server demands password authentication.
    +        However, <application>pg_repackdb</application> will waste a
    +        connection attempt finding out that the server wants a password.
    +        In some cases it is worth typing <option>-W</option> to avoid the =
    extra
    +        connection attempt.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +
    +     <varlistentry>
    +      <term><option>--maintenance-db=3D<replaceable class=3D"parameter">db=
    name</replaceable></option></term>
    +      <listitem>
    +       <para>
    +        When the <option>-a</option>/<option>--all</option> is used, conne=
    ct
    +        to this database to gather the list of databases to repack.
    +        If not specified, the <literal>postgres</literal> database will be=
     used,
    +        or if that does not exist, <literal>template1</literal> will be us=
    ed.
    +        This can be a <link linkend=3D"libpq-connstring">connection
    +        string</link>.  If so, connection string parameters will override =
    any
    +        conflicting command line options.  Also, connection string paramet=
    ers
    +        other than the database name itself will be re-used when connecting
    +        to other databases.
    +       </para>
    +      </listitem>
    +     </varlistentry>
    +    </variablelist>
    +   </para>
    + </refsect1>
    +
    +
    + <refsect1>
    +  <title>Environment</title>
    +
    +  <variablelist>
    +   <varlistentry>
    +    <term><envar>PGDATABASE</envar></term>
    +    <term><envar>PGHOST</envar></term>
    +    <term><envar>PGPORT</envar></term>
    +    <term><envar>PGUSER</envar></term>
    +
    +    <listitem>
    +     <para>
    +      Default connection parameters
    +     </para>
    +    </listitem>
    +   </varlistentry>
    +
    +   <varlistentry>
    +    <term><envar>PG_COLOR</envar></term>
    +    <listitem>
    +     <para>
    +      Specifies whether to use color in diagnostic messages. Possible valu=
    es
    +      are <literal>always</literal>, <literal>auto</literal> and
    +      <literal>never</literal>.
    +     </para>
    +    </listitem>
    +   </varlistentry>
    +  </variablelist>
    +
    +  <para>
    +   This utility, like most other <productname>PostgreSQL</productname> uti=
    lities,
    +   also uses the environment variables supported by <application>libpq</ap=
    plication>
    +   (see <xref linkend=3D"libpq-envars"/>).
    +  </para>
    +
    + </refsect1>
    +
    +
    + <refsect1>
    +  <title>Diagnostics</title>
    +
    +  <para>
    +   In case of difficulty, see
    +   <xref linkend=3D"sql-repack"/> and <xref linkend=3D"app-psql"/> for
    +   discussions of potential problems and error messages.
    +   The database server must be running at the
    +   targeted host.  Also, any default connection settings and environment
    +   variables used by the <application>libpq</application> front-end
    +   library will apply.
    +  </para>
    +
    + </refsect1>
    +
    + <refsect1>
    +  <title>Examples</title>
    +
    +   <para>
    +    To repack the database <literal>test</literal>:
    +<screen>
    +<prompt>$ </prompt><userinput>pg_repackdb test</userinput>
    +</screen>
    +   </para>
    +
    +   <para>
    +    To repack and analyze for the optimizer a database named
    +    <literal>bigdb</literal>:
    +<screen>
    +<prompt>$ </prompt><userinput>pg_repackdb --analyze bigdb</userinput>
    +</screen>
    +   </para>
    +
    +   <para>
    +    To repack a single table
    +    <literal>foo</literal> in a database named
    +    <literal>xyzzy</literal>, and analyze a single column
    +    <literal>bar</literal> of the table for the optimizer:
    +<screen>
    +<prompt>$ </prompt><userinput>pg_repackdb --analyze --verbose --table=3D'f=
    oo(bar)' xyzzy</userinput>
    +</screen></para>
    +
    +   <para>
    +    To repack all tables in the <literal>foo</literal> and <literal>bar</l=
    iteral> schemas
    +    in a database named <literal>xyzzy</literal>:
    +<screen>
    +<prompt>$ </prompt><userinput>pg_repackdb --schema=3D'foo' --schema=3D'bar=
    ' xyzzy</userinput>
    +</screen></para>
    +
    +
    + </refsect1>
    +
    + <refsect1>
    +  <title>See Also</title>
    +
    +  <simplelist type=3D"inline">
    +   <member><xref linkend=3D"sql-repack"/></member>
    +  </simplelist>
    + </refsect1>
    +
    +</refentry>
    diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml
    new file mode 100644
    index 00000000000..61d5c2cdef1
    --- /dev/null
    +++ b/doc/src/sgml/ref/repack.sgml
    @@ -0,0 +1,328 @@
    +<!--
    +doc/src/sgml/ref/repack.sgml
    +PostgreSQL documentation
    +-->
    +
    +<refentry id=3D"sql-repack">
    + <indexterm zone=3D"sql-repack">
    +  <primary>REPACK</primary>
    + </indexterm>
    +
    + <refmeta>
    +  <refentrytitle>REPACK</refentrytitle>
    +  <manvolnum>7</manvolnum>
    +  <refmiscinfo>SQL - Language Statements</refmiscinfo>
    + </refmeta>
    +
    + <refnamediv>
    +  <refname>REPACK</refname>
    +  <refpurpose>rewrite a table to reclaim disk space</refpurpose>
    + </refnamediv>
    +
    + <refsynopsisdiv>
    +<synopsis>
    +REPACK [ ( <replaceable class=3D"parameter">option</replaceable> [, ...] )=
     ] [ <replaceable class=3D"parameter">table_and_columns</replaceable> [ USI=
    NG INDEX [ <replaceable class=3D"parameter">index_name</replaceable> ] ] ]
    +REPACK [ ( <replaceable class=3D"parameter">option</replaceable> [, ...] )=
     ] USING INDEX
    +
    +<phrase>where <replaceable class=3D"parameter">option</replaceable> can be=
     one of:</phrase>
    +
    +    VERBOSE [ <replaceable class=3D"parameter">boolean</replaceable> ]
    +    ANALYZE [ <replaceable class=3D"parameter">boolean</replaceable> ]
    +
    +<phrase>and <replaceable class=3D"parameter">table_and_columns</replaceabl=
    e> is:</phrase>
    +
    +    <replaceable class=3D"parameter">table_name</replaceable> [ ( <replace=
    able class=3D"parameter">column_name</replaceable> [, ...] ) ]
    +</synopsis>
    + </refsynopsisdiv>
    +
    + <refsect1>
    +  <title>Description</title>
    +
    +  <para>
    +   <command>REPACK</command> reclaims storage occupied by dead
    +   tuples. Unlike <command>VACUUM</command>, it does so by rewriting the
    +   entire contents of the table specified
    +   by <replaceable class=3D"parameter">table_name</replaceable> into a new=
     disk
    +   file with no extra space (except for the space guaranteed by
    +   the <literal>fillfactor</literal> storage parameter), allowing unused s=
    pace
    +   to be returned to the operating system.
    +  </para>
    +
    +  <para>
    +   Without
    +   a <replaceable class=3D"parameter">table_name</replaceable>, <command>R=
    EPACK</command>
    +   processes every table and materialized view in the current database that
    +   the current user has the <literal>MAINTAIN</literal> privilege on. This
    +   form of <command>REPACK</command> cannot be executed inside a transacti=
    on
    +   block.
    +  </para>
    +
    +  <para>
    +   If a <literal>USING INDEX</literal> clause is specified, the rows are
    +   physically reordered based on information from an index.  Please see the
    +   notes on clustering below.
    +  </para>
    +
    +  <para>
    +   When a table is being repacked, an <literal>ACCESS EXCLUSIVE</literal> =
    lock
    +   is acquired on it. This prevents any other database operations (both re=
    ads
    +   and writes) from operating on the table until the <command>REPACK</comm=
    and>
    +   is finished.
    +  </para>
    +
    +  <refsect2 id=3D"sql-repack-notes-on-clustering" xreflabel=3D"Notes on Cl=
    ustering">
    +   <title>Notes on Clustering</title>
    +
    +   <para>
    +    If the <literal>USING INDEX</literal> clause is specified, the rows in
    +    the table are physically reordered following an index: if an index name
    +    is specified in the command, then that index is used; if no index name
    +    is specified, then the index that has been configured as the index to
    +    cluster on.  If no index has been configured in this way, an error is
    +    thrown.  The index given in the <literal>USING INDEX</literal> clause
    +    is configured as the index to cluster on, as well as an index given
    +    to the <command>CLUSTER</command> command.  An index can be set
    +    manually using <command>ALTER TABLE ... CLUSTER ON</command>, and reset
    +    with <command>ALTER TABLE ... SET WITHOUT CLUSTER</command>.
    +   </para>
    +
    +   <para>
    +    If no table name is specified in <command>REPACK USING INDEX</command>,
    +    all tables which have a clustering index defined and which the calling
    +    user has privileges for are processed.
    +   </para>
    +
    +   <para>
    +    Clustering is a one-time operation: when the table is
    +    subsequently updated, the changes are not clustered.  That is, no atte=
    mpt
    +    is made to store new or updated rows according to their index order.  =
    (If
    +    one wishes, one can periodically recluster by issuing the command agai=
    n.
    +    Also, setting the table's <literal>fillfactor</literal> storage parame=
    ter
    +    to less than 100% can aid in preserving cluster ordering during update=
    s,
    +    since updated rows are kept on the same page if enough space is availa=
    ble
    +    there.)
    +   </para>
    +
    +   <para>
    +    In cases where you are accessing single rows randomly within a table, =
    the
    +    actual order of the data in the table is unimportant. However, if you =
    tend
    +    to access some data more than others, and there is an index that groups
    +    them together, you will benefit from using clustering.  If
    +    you are requesting a range of indexed values from a table, or a single
    +    indexed value that has multiple rows that match,
    +    <command>REPACK</command> will help because once the index identifies =
    the
    +    table page for the first row that matches, all other rows that match a=
    re
    +    probably already on the same table page, and so you save disk accesses=
     and
    +    speed up the query.
    +   </para>
    +
    +   <para>
    +    <command>REPACK</command> can re-sort the table using either an index =
    scan
    +    on the specified index (if the index is a b-tree), or a sequential scan
    +    followed by sorting.  It will attempt to choose the method that will be
    +    faster, based on planner cost parameters and available statistical
    +    information.
    +   </para>
    +
    +   <para>
    +    Because the planner records statistics about the ordering of tables, i=
    t is
    +    advisable to
    +    run <link linkend=3D"sql-analyze"><command>ANALYZE</command></link> on=
     the
    +    newly repacked table.  Otherwise, the planner might make poor choices =
    of
    +    query plans.
    +   </para>
    +  </refsect2>
    +
    +  <refsect2 id=3D"sql-repack-notes-on-resources" xreflabel=3D"Notes on Res=
    ources">
    +   <title>Notes on Resources</title>
    +
    +   <para>
    +    When an index scan or a sequential scan without sort is used, a tempor=
    ary
    +    copy of the table is created that contains the table data in the index
    +    order.  Temporary copies of each index on the table are created as wel=
    l.
    +    Therefore, you need free space on disk at least equal to the sum of the
    +    table size and the index sizes.
    +   </para>
    +
    +   <para>
    +    When a sequential scan and sort is used, a temporary sort file is also
    +    created, so that the peak temporary space requirement is as much as do=
    uble
    +    the table size, plus the index sizes.  This method is often faster than
    +    the index scan method, but if the disk space requirement is intolerabl=
    e,
    +    you can disable this choice by temporarily setting
    +    <xref linkend=3D"guc-enable-sort"/> to <literal>off</literal>.
    +   </para>
    +
    +   <para>
    +    It is advisable to set <xref linkend=3D"guc-maintenance-work-mem"/> to=
     a
    +    reasonably large value (but not more than the amount of RAM you can
    +    dedicate to the <command>REPACK</command> operation) before repacking.
    +   </para>
    +  </refsect2>
    +
    + </refsect1>
    +
    + <refsect1>
    +  <title>Parameters</title>
    +
    +  <variablelist>
    +   <varlistentry>
    +    <term><replaceable class=3D"parameter">table_name</replaceable></term>
    +    <listitem>
    +     <para>
    +      The name (possibly schema-qualified) of a table.
    +     </para>
    +    </listitem>
    +   </varlistentry>
    +
    +   <varlistentry>
    +    <term><replaceable class=3D"parameter">column_name</replaceable></term>
    +    <listitem>
    +     <para>
    +      The name of a specific column to analyze. Defaults to all columns.
    +      If a column list is specific, <literal>ANALYZE</literal> must also
    +      be specified.
    +     </para>
    +    </listitem>
    +   </varlistentry>
    +
    +   <varlistentry>
    +    <term><replaceable class=3D"parameter">index_name</replaceable></term>
    +    <listitem>
    +     <para>
    +      The name of an index.
    +     </para>
    +    </listitem>
    +   </varlistentry>
    +
    +   <varlistentry>
    +    <term><literal>VERBOSE</literal></term>
    +    <listitem>
    +     <para>
    +      Prints a progress report as each table is repacked
    +      at <literal>INFO</literal> level.
    +     </para>
    +    </listitem>
    +   </varlistentry>
    +
    +   <varlistentry>
    +    <term><literal>ANALYZE</literal></term>
    +    <term><literal>ANALYSE</literal></term>
    +    <listitem>
    +     <para>
    +      Applies <xref linkend=3D"sql-analyze"/> on the table after repacking=
    .  This is
    +      currently only supported when a single (non-partitioned) table is sp=
    ecified.
    +     </para>
    +    </listitem>
    +   </varlistentry>
    +
    +   <varlistentry>
    +    <term><replaceable class=3D"parameter">boolean</replaceable></term>
    +    <listitem>
    +     <para>
    +      Specifies whether the selected option should be turned on or off.
    +      You can write <literal>TRUE</literal>, <literal>ON</literal>, or
    +      <literal>1</literal> to enable the option, and <literal>FALSE</liter=
    al>,
    +      <literal>OFF</literal>, or <literal>0</literal> to disable it.  The
    +      <replaceable class=3D"parameter">boolean</replaceable> value can also
    +      be omitted, in which case <literal>TRUE</literal> is assumed.
    +     </para>
    +    </listitem>
    +   </varlistentry>
    +  </variablelist>
    + </refsect1>
    +
    + <refsect1>
    +  <title>Notes</title>
    +
    +   <para>
    +    To repack a table, one must have the <literal>MAINTAIN</literal> privi=
    lege
    +    on the table.
    +   </para>
    +
    +   <para>
    +    While <command>REPACK</command> is running, the <xref
    +    linkend=3D"guc-search-path"/> is temporarily changed to <literal>pg_ca=
    talog,
    +    pg_temp</literal>.
    +   </para>
    +
    +  <para>
    +    Each backend running <command>REPACK</command> will report its progress
    +    in the <structname>pg_stat_progress_repack</structname> view. See
    +    <xref linkend=3D"repack-progress-reporting"/> for details.
    +  </para>
    +
    +   <para>
    +    Repacking a partitioned table repacks each of its partitions. If an in=
    dex
    +    is specified, each partition is repacked using the partition of that
    +    index. <command>REPACK</command> on a partitioned table cannot be exec=
    uted
    +    inside a transaction block.
    +   </para>
    +
    + </refsect1>
    +
    + <refsect1>
    +  <title>Examples</title>
    +
    +  <para>
    +   Repack the table <literal>employees</literal>:
    +<programlisting>
    +REPACK employees;
    +</programlisting>
    +  </para>
    +
    +  <para>
    +   Repack the table <literal>employees</literal> on the basis of its
    +   index <literal>employees_ind</literal> (Since index is used here, this =
    is
    +   effectively clustering):
    +<programlisting>
    +REPACK employees USING INDEX employees_ind;
    +</programlisting>
    +  </para>
    +
    +  <para>
    +   Repack the table <literal>cases</literal> on physical ordering,
    +   running an <command>ANALYZE</command> on the given columns once
    +   repacking is done, showing informational messages:
    +<programlisting>
    +REPACK (ANALYZE, VERBOSE) cases (district, case_nr);
    +</programlisting>
    +  </para>
    +
    +  <para>
    +   Repack all tables in the database on which you have
    +   the <literal>MAINTAIN</literal> privilege:
    +<programlisting>
    +REPACK;
    +</programlisting>
    +  </para>
    +
    +  <para>
    +   Repack all tables for which a clustering index has previously been
    +   configured on which you have the <literal>MAINTAIN</literal> privilege,
    +   showing informational messages:
    +<programlisting>
    +REPACK (VERBOSE) USING INDEX;
    +</programlisting>
    +  </para>
    +
    + </refsect1>
    +
    + <refsect1>
    +  <title>Compatibility</title>
    +
    +  <para>
    +   There is no <command>REPACK</command> statement in the SQL standard.
    +  </para>
    + </refsect1>
    +
    + <refsect1>
    +  <title>See Also</title>
    +
    +  <simplelist type=3D"inline">
    +   <member><xref linkend=3D"app-pgrepackdb"/></member>
    +   <member><xref linkend=3D"repack-progress-reporting"/></member>
    +  </simplelist>
    + </refsect1>
    +
    +</refentry>
    diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
    index 6d0fdd43cfb..ac5d083d468 100644
    --- a/doc/src/sgml/ref/vacuum.sgml
    +++ b/doc/src/sgml/ref/vacuum.sgml
    @@ -25,7 +25,6 @@ VACUUM [ ( <replaceable class=3D"parameter">option</repla=
    ceable> [, ...] ) ] [ <re
    =20
     <phrase>where <replaceable class=3D"parameter">option</replaceable> can be=
     one of:</phrase>
    =20
    -    FULL [ <replaceable class=3D"parameter">boolean</replaceable> ]
         FREEZE [ <replaceable class=3D"parameter">boolean</replaceable> ]
         VERBOSE [ <replaceable class=3D"parameter">boolean</replaceable> ]
         ANALYZE [ <replaceable class=3D"parameter">boolean</replaceable> ]
    @@ -39,6 +38,7 @@ VACUUM [ ( <replaceable class=3D"parameter">option</repla=
    ceable> [, ...] ) ] [ <re
         SKIP_DATABASE_STATS [ <replaceable class=3D"parameter">boolean</replac=
    eable> ]
         ONLY_DATABASE_STATS [ <replaceable class=3D"parameter">boolean</replac=
    eable> ]
         BUFFER_USAGE_LIMIT <replaceable class=3D"parameter">size</replaceable>
    +    FULL [ <replaceable class=3D"parameter">boolean</replaceable> ]
    =20
     <phrase>and <replaceable class=3D"parameter">table_and_columns</replaceabl=
    e> is:</phrase>
    =20
    @@ -95,20 +95,6 @@ VACUUM [ ( <replaceable class=3D"parameter">option</repl=
    aceable> [, ...] ) ] [ <re
       <title>Parameters</title>
    =20
       <variablelist>
    -   <varlistentry>
    -    <term><literal>FULL</literal></term>
    -    <listitem>
    -     <para>
    -      Selects <quote>full</quote> vacuum, which can reclaim more
    -      space, but takes much longer and exclusively locks the table.
    -      This method also requires extra disk space, since it writes a
    -      new copy of the table and doesn't release the old copy until
    -      the operation is complete.  Usually this should only be used when a
    -      significant amount of space needs to be reclaimed from within the ta=
    ble.
    -     </para>
    -    </listitem>
    -   </varlistentry>
    -
        <varlistentry>
         <term><literal>FREEZE</literal></term>
         <listitem>
    @@ -362,6 +348,23 @@ VACUUM [ ( <replaceable class=3D"parameter">option</re=
    placeable> [, ...] ) ] [ <re
         </listitem>
        </varlistentry>
    =20
    +   <varlistentry>
    +    <term><literal>FULL</literal></term>
    +    <listitem>
    +     <para>
    +      This option, which is deprecated, makes <command>VACUUM</command>
    +      behave like <command>REPACK</command> without a
    +      <literal>USING INDEX</literal> clause.
    +      This method of compacting the table takes much longer than
    +      <command>VACUUM</command> and exclusively locks the table.
    +      This method also requires extra disk space, since it writes a
    +      new copy of the table and doesn't release the old copy until
    +      the operation is complete.  Usually this should only be used when a
    +      significant amount of space needs to be reclaimed from within the ta=
    ble.
    +     </para>
    +    </listitem>
    +   </varlistentry>
    +
        <varlistentry>
         <term><replaceable class=3D"parameter">boolean</replaceable></term>
         <listitem>
    diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
    index 2cf02c37b17..5d9a8a25a02 100644
    --- a/doc/src/sgml/reference.sgml
    +++ b/doc/src/sgml/reference.sgml
    @@ -195,6 +195,7 @@
        &refreshMaterializedView;
        &reindex;
        &releaseSavepoint;
    +   &repack;
        &reset;
        &revoke;
        &rollback;
    @@ -258,6 +259,7 @@
        &pgIsready;
        &pgReceivewal;
        &pgRecvlogical;
    +   &pgRepackdb;
        &pgRestore;
        &pgVerifyBackup;
        &psqlRef;
    diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/=
    heap/heapam_handler.c
    index bcbac844bb6..79f9de5d760 100644
    --- a/src/backend/access/heap/heapam_handler.c
    +++ b/src/backend/access/heap/heapam_handler.c
    @@ -741,13 +741,13 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Re=
    lation NewHeap,
     	if (OldIndex !=3D NULL && !use_sort)
     	{
     		const int	ci_index[] =3D {
    -			PROGRESS_CLUSTER_PHASE,
    -			PROGRESS_CLUSTER_INDEX_RELID
    +			PROGRESS_REPACK_PHASE,
    +			PROGRESS_REPACK_INDEX_RELID
     		};
     		int64		ci_val[2];
    =20
     		/* Set phase and OIDOldIndex to columns */
    -		ci_val[0] =3D PROGRESS_CLUSTER_PHASE_INDEX_SCAN_HEAP;
    +		ci_val[0] =3D PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP;
     		ci_val[1] =3D RelationGetRelid(OldIndex);
     		pgstat_progress_update_multi_param(2, ci_index, ci_val);
    =20
    @@ -759,15 +759,15 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Re=
    lation NewHeap,
     	else
     	{
     		/* In scan-and-sort mode and also VACUUM FULL, set phase */
    -		pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
    -									 PROGRESS_CLUSTER_PHASE_SEQ_SCAN_HEAP);
    +		pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
    +									 PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP);
    =20
     		tableScan =3D table_beginscan(OldHeap, SnapshotAny, 0, (ScanKey) NULL);
     		heapScan =3D (HeapScanDesc) tableScan;
     		indexScan =3D NULL;
    =20
     		/* Set total heap blocks */
    -		pgstat_progress_update_param(PROGRESS_CLUSTER_TOTAL_HEAP_BLKS,
    +		pgstat_progress_update_param(PROGRESS_REPACK_TOTAL_HEAP_BLKS,
     									 heapScan->rs_nblocks);
     	}
    =20
    @@ -809,7 +809,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Rela=
    tion NewHeap,
     				 * is manually updated to the correct value when the table
     				 * scan finishes.
     				 */
    -				pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED,
    +				pgstat_progress_update_param(PROGRESS_REPACK_HEAP_BLKS_SCANNED,
     											 heapScan->rs_nblocks);
     				break;
     			}
    @@ -825,7 +825,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Rela=
    tion NewHeap,
     			 */
     			if (prev_cblock !=3D heapScan->rs_cblock)
     			{
    -				pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED,
    +				pgstat_progress_update_param(PROGRESS_REPACK_HEAP_BLKS_SCANNED,
     											 (heapScan->rs_cblock +
     											  heapScan->rs_nblocks -
     											  heapScan->rs_startblock
    @@ -912,14 +912,14 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Re=
    lation NewHeap,
     			 * In scan-and-sort mode, report increase in number of tuples
     			 * scanned
     			 */
    -			pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED,
    +			pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_SCANNED,
     										 *num_tuples);
     		}
     		else
     		{
     			const int	ct_index[] =3D {
    -				PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED,
    -				PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN
    +				PROGRESS_REPACK_HEAP_TUPLES_SCANNED,
    +				PROGRESS_REPACK_HEAP_TUPLES_WRITTEN
     			};
     			int64		ct_val[2];
    =20
    @@ -952,14 +952,14 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Re=
    lation NewHeap,
     		double		n_tuples =3D 0;
    =20
     		/* Report that we are now sorting tuples */
    -		pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
    -									 PROGRESS_CLUSTER_PHASE_SORT_TUPLES);
    +		pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
    +									 PROGRESS_REPACK_PHASE_SORT_TUPLES);
    =20
     		tuplesort_performsort(tuplesort);
    =20
     		/* Report that we are now writing new heap */
    -		pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
    -									 PROGRESS_CLUSTER_PHASE_WRITE_NEW_HEAP);
    +		pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
    +									 PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP);
    =20
     		for (;;)
     		{
    @@ -977,7 +977,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Rela=
    tion NewHeap,
     									 values, isnull,
     									 rwstate);
     			/* Report n_tuples */
    -			pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN,
    +			pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_WRITTEN,
     										 n_tuples);
     		}
    =20
    diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
    index 5d9db167e59..08d4b8e44d7 100644
    --- a/src/backend/catalog/index.c
    +++ b/src/backend/catalog/index.c
    @@ -4079,7 +4079,7 @@ reindex_relation(const ReindexStmt *stmt, Oid relid, =
    int flags,
     		Assert(!ReindexIsProcessingIndex(indexOid));
    =20
     		/* Set index rebuild count */
    -		pgstat_progress_update_param(PROGRESS_CLUSTER_INDEX_REBUILD_COUNT,
    +		pgstat_progress_update_param(PROGRESS_REPACK_INDEX_REBUILD_COUNT,
     									 i);
     		i++;
     	}
    diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/sys=
    tem_views.sql
    index 086c4c8fb6f..024d219016d 100644
    --- a/src/backend/catalog/system_views.sql
    +++ b/src/backend/catalog/system_views.sql
    @@ -1272,14 +1272,15 @@ CREATE VIEW pg_stat_progress_vacuum AS
         FROM pg_stat_get_progress_info('VACUUM') AS S
             LEFT JOIN pg_database D ON S.datid =3D D.oid;
    =20
    -CREATE VIEW pg_stat_progress_cluster AS
    +CREATE VIEW pg_stat_progress_repack AS
         SELECT
             S.pid AS pid,
             S.datid AS datid,
             D.datname AS datname,
             S.relid AS relid,
             CASE S.param1 WHEN 1 THEN 'CLUSTER'
    -                      WHEN 2 THEN 'VACUUM FULL'
    +                      WHEN 2 THEN 'REPACK'
    +                      WHEN 3 THEN 'VACUUM FULL'
                           END AS command,
             CASE S.param2 WHEN 0 THEN 'initializing'
                           WHEN 1 THEN 'seq scanning heap'
    @@ -1290,15 +1291,35 @@ CREATE VIEW pg_stat_progress_cluster AS
                           WHEN 6 THEN 'rebuilding index'
                           WHEN 7 THEN 'performing final cleanup'
                           END AS phase,
    -        CAST(S.param3 AS oid) AS cluster_index_relid,
    +        CAST(S.param3 AS oid) AS repack_index_relid,
             S.param4 AS heap_tuples_scanned,
             S.param5 AS heap_tuples_written,
             S.param6 AS heap_blks_total,
             S.param7 AS heap_blks_scanned,
             S.param8 AS index_rebuild_count
    -    FROM pg_stat_get_progress_info('CLUSTER') AS S
    +    FROM pg_stat_get_progress_info('REPACK') AS S
             LEFT JOIN pg_database D ON S.datid =3D D.oid;
    =20
    +-- This view is as the one above, except for renaming a column and avoiding
    +-- 'REPACK' as a command name to report.
    +CREATE VIEW pg_stat_progress_cluster AS
    +    SELECT
    +        pid,
    +        datid,
    +        datname,
    +        relid,
    +        CASE WHEN command IN ('CLUSTER', 'VACUUM FULL') THEN command
    +             WHEN repack_index_relid =3D 0 THEN 'VACUUM FULL'
    +             ELSE 'CLUSTER' END AS command,
    +        phase,
    +        repack_index_relid AS cluster_index_relid,
    +        heap_tuples_scanned,
    +        heap_tuples_written,
    +        heap_blks_total,
    +        heap_blks_scanned,
    +        index_rebuild_count
    +    FROM pg_stat_progress_repack;
    +
     CREATE VIEW pg_stat_progress_create_index AS
         SELECT
             S.pid AS pid, S.datid AS datid, D.datname AS datname,
    diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
    index b55221d44cd..ba3c076ea7d 100644
    --- a/src/backend/commands/cluster.c
    +++ b/src/backend/commands/cluster.c
    @@ -1,7 +1,8 @@
     /*-------------------------------------------------------------------------
      *
      * cluster.c
    - *	  CLUSTER a table on an index.  This is now also used for VACUUM FULL.
    + *	  CLUSTER a table on an index.  This is now also used for VACUUM FULL a=
    nd
    + *	  REPACK.
      *
      * There is hardly anything left of Paul Brown's original implementation...
      *
    @@ -67,27 +68,36 @@ typedef struct
     	Oid			indexOid;
     } RelToCluster;
    =20
    -
    -static void cluster_multiple_rels(List *rtcs, ClusterParams *params);
    -static void rebuild_relation(Relation OldHeap, Relation index, bool verbos=
    e);
    +static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
    +								Oid indexOid, Oid userid, int options);
    +static void rebuild_relation(RepackCommand cmd,
    +							 Relation OldHeap, Relation index, bool verbose);
     static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation O=
    ldIndex,
     							bool verbose, bool *pSwapToastByContent,
     							TransactionId *pFreezeXid, MultiXactId *pCutoffMulti);
    -static List *get_tables_to_cluster(MemoryContext cluster_context);
    -static List *get_tables_to_cluster_partitioned(MemoryContext cluster_conte=
    xt,
    -											   Oid indexOid);
    -static bool cluster_is_permitted_for_relation(Oid relid, Oid userid);
    +static List *get_tables_to_repack(RepackCommand cmd, bool usingindex,
    +								  MemoryContext permcxt);
    +static List *get_tables_to_repack_partitioned(RepackCommand cmd,
    +											  Oid relid, bool rel_is_index,
    +											  MemoryContext permcxt);
    +static bool cluster_is_permitted_for_relation(RepackCommand cmd,
    +											  Oid relid, Oid userid);
    +static Relation process_single_relation(RepackStmt *stmt,
    +										ClusterParams *params);
    +static Oid	determine_clustered_index(Relation rel, bool usingindex,
    +									  const char *indexname);
    +static const char *RepackCommandAsString(RepackCommand cmd);
    =20
    =20
    -/*------------------------------------------------------------------------=
    ---
    - * This cluster code allows for clustering multiple tables at once. Because
    +/*
    + * The repack code allows for processing multiple tables at once. Because
      * of this, we cannot just run everything on a single transaction, or we
      * would be forced to acquire exclusive locks on all the tables being
      * clustered, simultaneously --- very likely leading to deadlock.
      *
    - * To solve this we follow a similar strategy to VACUUM code,
    - * clustering each relation in a separate transaction. For this to work,
    - * we need to:
    + * To solve this we follow a similar strategy to VACUUM code, processing e=
    ach
    + * relation in a separate transaction. For this to work, we need to:
    + *
      *	- provide a separate memory context so that we can pass information in
      *	  a way that survives across transactions
      *	- start a new transaction every time a new relation is clustered
    @@ -98,197 +108,165 @@ static bool cluster_is_permitted_for_relation(Oid re=
    lid, Oid userid);
      *
      * The single-relation case does not have any such overhead.
      *
    - * We also allow a relation to be specified without index.  In that case,
    - * the indisclustered bit will be looked up, and an ERROR will be thrown
    - * if there is no index with the bit set.
    - *------------------------------------------------------------------------=
    ---
    + * We also allow a relation to be repacked following an index, but without
    + * naming a specific one.  In that case, the indisclustered bit will be
    + * looked up, and an ERROR will be thrown if no so-marked index is found.
      */
     void
    -cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel)
    +ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
     {
    -	ListCell   *lc;
     	ClusterParams params =3D {0};
    -	bool		verbose =3D false;
     	Relation	rel =3D NULL;
    -	Oid			indexOid =3D InvalidOid;
    -	MemoryContext cluster_context;
    +	MemoryContext repack_context;
     	List	   *rtcs;
    =20
     	/* Parse option list */
    -	foreach(lc, stmt->params)
    +	foreach_node(DefElem, opt, stmt->params)
     	{
    -		DefElem    *opt =3D (DefElem *) lfirst(lc);
    -
     		if (strcmp(opt->defname, "verbose") =3D=3D 0)
    -			verbose =3D defGetBoolean(opt);
    +			params.options |=3D defGetBoolean(opt) ? CLUOPT_VERBOSE : 0;
    +		else if (strcmp(opt->defname, "analyze") =3D=3D 0 ||
    +				 strcmp(opt->defname, "analyse") =3D=3D 0)
    +			params.options |=3D defGetBoolean(opt) ? CLUOPT_ANALYZE : 0;
     		else
     			ereport(ERROR,
    -					(errcode(ERRCODE_SYNTAX_ERROR),
    -					 errmsg("unrecognized CLUSTER option \"%s\"",
    -							opt->defname),
    -					 parser_errposition(pstate, opt->location)));
    +					errcode(ERRCODE_SYNTAX_ERROR),
    +					errmsg("unrecognized %s option \"%s\"",
    +						   RepackCommandAsString(stmt->command),
    +						   opt->defname),
    +					parser_errposition(pstate, opt->location));
     	}
    =20
    -	params.options =3D (verbose ? CLUOPT_VERBOSE : 0);
    -
    +	/*
    +	 * If a single relation is specified, process it and we're done ... unless
    +	 * the relation is a partitioned table, in which case we fall through.
    +	 */
     	if (stmt->relation !=3D NULL)
     	{
    -		/* This is the single-relation case. */
    -		Oid			tableOid;
    -
    -		/*
    -		 * Find, lock, and check permissions on the table.  We obtain
    -		 * AccessExclusiveLock right away to avoid lock-upgrade hazard in the
    -		 * single-transaction case.
    -		 */
    -		tableOid =3D RangeVarGetRelidExtended(stmt->relation,
    -											AccessExclusiveLock,
    -											0,
    -											RangeVarCallbackMaintainsTable,
    -											NULL);
    -		rel =3D table_open(tableOid, NoLock);
    -
    -		/*
    -		 * Reject clustering a remote temp table ... their local buffer
    -		 * manager is not going to cope.
    -		 */
    -		if (RELATION_IS_OTHER_TEMP(rel))
    -			ereport(ERROR,
    -					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    -					 errmsg("cannot cluster temporary tables of other sessions")));
    -
    -		if (stmt->indexname =3D=3D NULL)
    -		{
    -			ListCell   *index;
    -
    -			/* We need to find the index that has indisclustered set. */
    -			foreach(index, RelationGetIndexList(rel))
    -			{
    -				indexOid =3D lfirst_oid(index);
    -				if (get_index_isclustered(indexOid))
    -					break;
    -				indexOid =3D InvalidOid;
    -			}
    -
    -			if (!OidIsValid(indexOid))
    -				ereport(ERROR,
    -						(errcode(ERRCODE_UNDEFINED_OBJECT),
    -						 errmsg("there is no previously clustered index for table \"%s\"",
    -								stmt->relation->relname)));
    -		}
    -		else
    -		{
    -			/*
    -			 * The index is expected to be in the same namespace as the
    -			 * relation.
    -			 */
    -			indexOid =3D get_relname_relid(stmt->indexname,
    -										 rel->rd_rel->relnamespace);
    -			if (!OidIsValid(indexOid))
    -				ereport(ERROR,
    -						(errcode(ERRCODE_UNDEFINED_OBJECT),
    -						 errmsg("index \"%s\" for table \"%s\" does not exist",
    -								stmt->indexname, stmt->relation->relname)));
    -		}
    -
    -		/* For non-partitioned tables, do what we came here to do. */
    -		if (rel->rd_rel->relkind !=3D RELKIND_PARTITIONED_TABLE)
    -		{
    -			cluster_rel(rel, indexOid, &params);
    -			/* cluster_rel closes the relation, but keeps lock */
    -
    -			return;
    -		}
    +		rel =3D process_single_relation(stmt, &params);
    +		if (rel =3D=3D NULL)
    +			return;				/* all done */
     	}
    =20
    +	/*
    +	 * Don't allow ANALYZE in the multiple-relation case for now.  Maybe we
    +	 * can add support for this later.
    +	 */
    +	if (params.options & CLUOPT_ANALYZE)
    +		ereport(ERROR,
    +				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    +				errmsg("cannot %s multiple tables", "REPACK (ANALYZE)"));
    +
     	/*
     	 * By here, we know we are in a multi-table situation.  In order to avoid
     	 * holding locks for too long, we want to process each table in its own
     	 * transaction.  This forces us to disallow running inside a user
     	 * transaction block.
     	 */
    -	PreventInTransactionBlock(isTopLevel, "CLUSTER");
    +	PreventInTransactionBlock(isTopLevel, RepackCommandAsString(stmt->command=
    ));
    =20
     	/* Also, we need a memory context to hold our list of relations */
    -	cluster_context =3D AllocSetContextCreate(PortalContext,
    -											"Cluster",
    -											ALLOCSET_DEFAULT_SIZES);
    +	repack_context =3D AllocSetContextCreate(PortalContext,
    +										   "Repack",
    +										   ALLOCSET_DEFAULT_SIZES);
    +
    +	params.options |=3D CLUOPT_RECHECK;
    =20
     	/*
    -	 * Either we're processing a partitioned table, or we were not given any
    -	 * table name at all.  In either case, obtain a list of relations to
    -	 * process.
    -	 *
    -	 * In the former case, an index name must have been given, so we don't
    -	 * need to recheck its "indisclustered" bit, but we have to check that it
    -	 * is an index that we can cluster on.  In the latter case, we set the
    -	 * option bit to have indisclustered verified.
    -	 *
    -	 * Rechecking the relation itself is necessary here in all cases.
    +	 * If we don't have a relation yet, determine a relation list.  If we do,
    +	 * then it must be a partitioned table, and we want to process its
    +	 * partitions.
     	 */
    -	params.options |=3D CLUOPT_RECHECK;
    -	if (rel !=3D NULL)
    +	if (rel =3D=3D NULL)
     	{
    -		Assert(rel->rd_rel->relkind =3D=3D RELKIND_PARTITIONED_TABLE);
    -		check_index_is_clusterable(rel, indexOid, AccessShareLock);
    -		rtcs =3D get_tables_to_cluster_partitioned(cluster_context, indexOid);
    -
    -		/* close relation, releasing lock on parent table */
    -		table_close(rel, AccessExclusiveLock);
    +		Assert(stmt->indexname =3D=3D NULL);
    +		rtcs =3D get_tables_to_repack(stmt->command, stmt->usingindex,
    +									repack_context);
     	}
     	else
     	{
    -		rtcs =3D get_tables_to_cluster(cluster_context);
    -		params.options |=3D CLUOPT_RECHECK_ISCLUSTERED;
    -	}
    +		Oid			relid;
    +		bool		rel_is_index;
    =20
    -	/* Do the job. */
    -	cluster_multiple_rels(rtcs, &params);
    +		Assert(rel->rd_rel->relkind =3D=3D RELKIND_PARTITIONED_TABLE);
    =20
    -	/* Start a new transaction for the cleanup work. */
    -	StartTransactionCommand();
    +		/*
    +		 * If USING INDEX was specified, resolve the index name now and pass
    +		 * it down.
    +		 */
    +		if (stmt->usingindex)
    +		{
    +			/*
    +			 * If no index name was specified when repacking a partitioned
    +			 * table, punt for now.  Maybe we can improve this later.
    +			 */
    +			if (!stmt->indexname)
    +				ereport(ERROR,
    +						errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
    +						errmsg("there is no previously clustered index for table \"%s\"",
    +							   RelationGetRelationName(rel)));
    +
    +			relid =3D determine_clustered_index(rel, stmt->usingindex,
    +											  stmt->indexname);
    +			if (!OidIsValid(relid))
    +				elog(ERROR, "unable to determine index to cluster on");
    +			/* XXX is this the right place for this check? */
    +			check_index_is_clusterable(rel, relid, AccessExclusiveLock);
    +			rel_is_index =3D true;
    +		}
    +		else
    +		{
    +			relid =3D RelationGetRelid(rel);
    +			rel_is_index =3D false;
    +		}
    =20
    -	/* Clean up working storage */
    -	MemoryContextDelete(cluster_context);
    -}
    +		rtcs =3D get_tables_to_repack_partitioned(stmt->command,
    +												relid, rel_is_index,
    +												repack_context);
    =20
    -/*
    - * Given a list of relations to cluster, process each of them in a separate
    - * transaction.
    - *
    - * We expect to be in a transaction at start, but there isn't one when we
    - * return.
    - */
    -static void
    -cluster_multiple_rels(List *rtcs, ClusterParams *params)
    -{
    -	ListCell   *lc;
    +		/* close parent relation, releasing lock on it */
    +		table_close(rel, AccessExclusiveLock);
    +		rel =3D NULL;
    +	}
    =20
     	/* Commit to get out of starting transaction */
     	PopActiveSnapshot();
     	CommitTransactionCommand();
    =20
     	/* Cluster the tables, each in a separate transaction */
    -	foreach(lc, rtcs)
    +	Assert(rel =3D=3D NULL);
    +	foreach_ptr(RelToCluster, rtc, rtcs)
     	{
    -		RelToCluster *rtc =3D (RelToCluster *) lfirst(lc);
    -		Relation	rel;
    -
     		/* Start a new transaction for each relation. */
     		StartTransactionCommand();
    =20
    +		/*
    +		 * Open the target table, coping with the case where it has been
    +		 * dropped.
    +		 */
    +		rel =3D try_table_open(rtc->tableOid, AccessExclusiveLock);
    +		if (rel =3D=3D NULL)
    +		{
    +			CommitTransactionCommand();
    +			continue;
    +		}
    +
     		/* functions in indexes may want a snapshot set */
     		PushActiveSnapshot(GetTransactionSnapshot());
    =20
    -		rel =3D table_open(rtc->tableOid, AccessExclusiveLock);
    -
     		/* Process this table */
    -		cluster_rel(rel, rtc->indexOid, params);
    +		cluster_rel(stmt->command, rel, rtc->indexOid, &params);
     		/* cluster_rel closes the relation, but keeps lock */
    =20
     		PopActiveSnapshot();
     		CommitTransactionCommand();
     	}
    +
    +	/* Start a new transaction for the cleanup work. */
    +	StartTransactionCommand();
    +
    +	/* Clean up working storage */
    +	MemoryContextDelete(repack_context);
     }
    =20
     /*
    @@ -304,11 +282,14 @@ cluster_multiple_rels(List *rtcs, ClusterParams *para=
    ms)
      * them incrementally while we load the table.
      *
      * If indexOid is InvalidOid, the table will be rewritten in physical order
    - * instead of index order.  This is the new implementation of VACUUM FULL,
    - * and error messages should refer to the operation as VACUUM not CLUSTER.
    + * instead of index order.
    + *
    + * 'cmd' indicates which command is being executed, to be used for error
    + * messages.
      */
     void
    -cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params)
    +cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
    +			ClusterParams *params)
     {
     	Oid			tableOid =3D RelationGetRelid(OldHeap);
     	Oid			save_userid;
    @@ -323,13 +304,8 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterPar=
    ams *params)
     	/* Check for user-requested abort. */
     	CHECK_FOR_INTERRUPTS();
    =20
    -	pgstat_progress_start_command(PROGRESS_COMMAND_CLUSTER, tableOid);
    -	if (OidIsValid(indexOid))
    -		pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND,
    -									 PROGRESS_CLUSTER_COMMAND_CLUSTER);
    -	else
    -		pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND,
    -									 PROGRESS_CLUSTER_COMMAND_VACUUM_FULL);
    +	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
    +	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
    =20
     	/*
     	 * Switch to the table owner's userid, so that any index functions are run
    @@ -350,86 +326,38 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterPa=
    rams *params)
     	 * *must* skip the one on indisclustered since it would reject an attempt
     	 * to cluster a not-previously-clustered index.
     	 */
    -	if (recheck)
    -	{
    -		/* Check that the user still has privileges for the relation */
    -		if (!cluster_is_permitted_for_relation(tableOid, save_userid))
    -		{
    -			relation_close(OldHeap, AccessExclusiveLock);
    -			goto out;
    -		}
    -
    -		/*
    -		 * Silently skip a temp table for a remote session.  Only doing this
    -		 * check in the "recheck" case is appropriate (which currently means
    -		 * somebody is executing a database-wide CLUSTER or on a partitioned
    -		 * table), because there is another check in cluster() which will stop
    -		 * any attempt to cluster remote temp tables by name.  There is
    -		 * another check in cluster_rel which is redundant, but we leave it
    -		 * for extra safety.
    -		 */
    -		if (RELATION_IS_OTHER_TEMP(OldHeap))
    -		{
    -			relation_close(OldHeap, AccessExclusiveLock);
    -			goto out;
    -		}
    -
    -		if (OidIsValid(indexOid))
    -		{
    -			/*
    -			 * Check that the index still exists
    -			 */
    -			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid)))
    -			{
    -				relation_close(OldHeap, AccessExclusiveLock);
    -				goto out;
    -			}
    -
    -			/*
    -			 * Check that the index is still the one with indisclustered set,
    -			 * if needed.
    -			 */
    -			if ((params->options & CLUOPT_RECHECK_ISCLUSTERED) !=3D 0 &&
    -				!get_index_isclustered(indexOid))
    -			{
    -				relation_close(OldHeap, AccessExclusiveLock);
    -				goto out;
    -			}
    -		}
    -	}
    +	if (recheck &&
    +		!cluster_rel_recheck(cmd, OldHeap, indexOid, save_userid,
    +							 params->options))
    +		goto out;
    =20
     	/*
    -	 * We allow VACUUM FULL, but not CLUSTER, on shared catalogs.  CLUSTER
    -	 * would work in most respects, but the index would only get marked as
    -	 * indisclustered in the current database, leading to unexpected behavior
    -	 * if CLUSTER were later invoked in another database.
    +	 * We allow repacking shared catalogs only when not using an index. It
    +	 * would work to use an index in most respects, but the index would only
    +	 * get marked as indisclustered in the current database, leading to
    +	 * unexpected behavior if CLUSTER were later invoked in another database.
     	 */
     	if (OidIsValid(indexOid) && OldHeap->rd_rel->relisshared)
     		ereport(ERROR,
    -				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    -				 errmsg("cannot cluster a shared catalog")));
    +				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    +				errmsg("cannot run %s on a shared catalog",
    +					   RepackCommandAsString(cmd)));
    =20
     	/*
     	 * Don't process temp tables of other backends ... their local buffer
     	 * manager is not going to cope.
     	 */
     	if (RELATION_IS_OTHER_TEMP(OldHeap))
    -	{
    -		if (OidIsValid(indexOid))
    -			ereport(ERROR,
    -					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    -					 errmsg("cannot cluster temporary tables of other sessions")));
    -		else
    -			ereport(ERROR,
    -					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    -					 errmsg("cannot vacuum temporary tables of other sessions")));
    -	}
    +		ereport(ERROR,
    +				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    +				errmsg("cannot run %s on temporary tables of other sessions",
    +					   RepackCommandAsString(cmd)));
    =20
     	/*
     	 * Also check for active uses of the relation in the current transaction,
     	 * including open scans and pending AFTER trigger events.
     	 */
    -	CheckTableNotInUse(OldHeap, OidIsValid(indexOid) ? "CLUSTER" : "VACUUM");
    +	CheckTableNotInUse(OldHeap, RepackCommandAsString(cmd));
    =20
     	/* Check heap and index are valid to cluster on */
     	if (OidIsValid(indexOid))
    @@ -442,6 +370,24 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterPar=
    ams *params)
     	else
     		index =3D NULL;
    =20
    +	/*
    +	 * When allow_system_table_mods is turned off, we disallow repacking a
    +	 * catalog on a particular index unless that's already the clustered index
    +	 * for that catalog.
    +	 *
    +	 * XXX We don't check for this in CLUSTER, because it's historically been
    +	 * allowed.
    +	 */
    +	if (cmd !=3D REPACK_COMMAND_CLUSTER &&
    +		!allowSystemTableMods && OidIsValid(indexOid) &&
    +		IsCatalogRelation(OldHeap) && !index->rd_index->indisclustered)
    +		ereport(ERROR,
    +				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
    +				errmsg("permission denied: \"%s\" is a system catalog",
    +					   RelationGetRelationName(OldHeap)),
    +				errdetail("System catalogs can only be clustered by the index they're =
    already clustered on, if any, unless \"%s\" is enabled.",
    +						  "allow_system_table_mods"));
    +
     	/*
     	 * Quietly ignore the request if this is a materialized view which has not
     	 * been populated from its query. No harm is done because there is no data
    @@ -469,7 +415,7 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterPara=
    ms *params)
     	TransferPredicateLocksToHeapRelation(OldHeap);
    =20
     	/* rebuild_relation does all the dirty work */
    -	rebuild_relation(OldHeap, index, verbose);
    +	rebuild_relation(cmd, OldHeap, index, verbose);
     	/* rebuild_relation closes OldHeap, and index if valid */
    =20
     out:
    @@ -482,6 +428,63 @@ out:
     	pgstat_progress_end_command();
     }
    =20
    +/*
    + * Check if the table (and its index) still meets the requirements of
    + * cluster_rel().
    + */
    +static bool
    +cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
    +					Oid userid, int options)
    +{
    +	Oid			tableOid =3D RelationGetRelid(OldHeap);
    +
    +	/* Check that the user still has privileges for the relation */
    +	if (!cluster_is_permitted_for_relation(cmd, tableOid, userid))
    +	{
    +		relation_close(OldHeap, AccessExclusiveLock);
    +		return false;
    +	}
    +
    +	/*
    +	 * Silently skip a temp table for a remote session.  Only doing this check
    +	 * in the "recheck" case is appropriate (which currently means somebody is
    +	 * executing a database-wide CLUSTER or on a partitioned table), because
    +	 * there is another check in cluster() which will stop any attempt to
    +	 * cluster remote temp tables by name.  There is another check in
    +	 * cluster_rel which is redundant, but we leave it for extra safety.
    +	 */
    +	if (RELATION_IS_OTHER_TEMP(OldHeap))
    +	{
    +		relation_close(OldHeap, AccessExclusiveLock);
    +		return false;
    +	}
    +
    +	if (OidIsValid(indexOid))
    +	{
    +		/*
    +		 * Check that the index still exists
    +		 */
    +		if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid)))
    +		{
    +			relation_close(OldHeap, AccessExclusiveLock);
    +			return false;
    +		}
    +
    +		/*
    +		 * Check that the index is still the one with indisclustered set, if
    +		 * needed.
    +		 */
    +		if ((options & CLUOPT_RECHECK_ISCLUSTERED) !=3D 0 &&
    +			!get_index_isclustered(indexOid))
    +		{
    +			relation_close(OldHeap, AccessExclusiveLock);
    +			return false;
    +		}
    +	}
    +
    +	return true;
    +}
    +
     /*
      * Verify that the specified heap and index are valid to cluster on
      *
    @@ -626,7 +629,8 @@ mark_index_clustered(Relation rel, Oid indexOid, bool i=
    s_internal)
      * On exit, they are closed, but locks on them are not released.
      */
     static void
    -rebuild_relation(Relation OldHeap, Relation index, bool verbose)
    +rebuild_relation(RepackCommand cmd,
    +				 Relation OldHeap, Relation index, bool verbose)
     {
     	Oid			tableOid =3D RelationGetRelid(OldHeap);
     	Oid			accessMethod =3D OldHeap->rd_rel->relam;
    @@ -642,8 +646,8 @@ rebuild_relation(Relation OldHeap, Relation index, bool=
     verbose)
     	Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false) &&
     		   (index =3D=3D NULL || CheckRelationLockedByMe(index, AccessExclusiveL=
    ock, false)));
    =20
    -	if (index)
    -		/* Mark the correct index as clustered */
    +	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
    +	if (index !=3D NULL)
     		mark_index_clustered(OldHeap, RelationGetRelid(index), true);
    =20
     	/* Remember info about rel before closing OldHeap */
    @@ -958,20 +962,20 @@ copy_table_data(Relation NewHeap, Relation OldHeap, R=
    elation OldIndex, bool verb
     	/* Log what we're doing */
     	if (OldIndex !=3D NULL && !use_sort)
     		ereport(elevel,
    -				(errmsg("clustering \"%s.%s\" using index scan on \"%s\"",
    -						nspname,
    -						RelationGetRelationName(OldHeap),
    -						RelationGetRelationName(OldIndex))));
    +				errmsg("repacking \"%s.%s\" using index scan on \"%s\"",
    +					   nspname,
    +					   RelationGetRelationName(OldHeap),
    +					   RelationGetRelationName(OldIndex)));
     	else if (use_sort)
     		ereport(elevel,
    -				(errmsg("clustering \"%s.%s\" using sequential scan and sort",
    -						nspname,
    -						RelationGetRelationName(OldHeap))));
    +				errmsg("repacking \"%s.%s\" using sequential scan and sort",
    +					   nspname,
    +					   RelationGetRelationName(OldHeap)));
     	else
     		ereport(elevel,
    -				(errmsg("vacuuming \"%s.%s\"",
    -						nspname,
    -						RelationGetRelationName(OldHeap))));
    +				errmsg("repacking \"%s.%s\" in physical order",
    +					   nspname,
    +					   RelationGetRelationName(OldHeap)));
    =20
     	/*
     	 * Hand off the actual copying to AM specific function, the generic code
    @@ -1458,8 +1462,8 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
     	int			i;
    =20
     	/* Report that we are now swapping relation files */
    -	pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
    -								 PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES);
    +	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
    +								 PROGRESS_REPACK_PHASE_SWAP_REL_FILES);
    =20
     	/* Zero out possible results from swapped_relation_files */
     	memset(mapped_tables, 0, sizeof(mapped_tables));
    @@ -1509,14 +1513,14 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
     		reindex_flags |=3D REINDEX_REL_FORCE_INDEXES_PERMANENT;
    =20
     	/* Report that we are now reindexing relations */
    -	pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
    -								 PROGRESS_CLUSTER_PHASE_REBUILD_INDEX);
    +	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
    +								 PROGRESS_REPACK_PHASE_REBUILD_INDEX);
    =20
     	reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params);
    =20
     	/* Report that we are now doing clean up */
    -	pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
    -								 PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP);
    +	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
    +								 PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
    =20
     	/*
     	 * If the relation being rebuilt is pg_class, swap_relation_files()
    @@ -1632,106 +1636,191 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
     	}
     }
    =20
    -
     /*
    - * Get a list of tables that the current user has privileges on and
    - * have indisclustered set.  Return the list in a List * of RelToCluster
    - * (stored in the specified memory context), each one giving the tableOid
    - * and the indexOid on which the table is already clustered.
    + * Determine which relations to process, when REPACK/CLUSTER is called
    + * without specifying a table name.  The exact process depends on whether
    + * USING INDEX was given or not, and in any case we only return tables and
    + * materialized views that the current user has privileges to repack/clust=
    er.
    + *
    + * If USING INDEX was given, we scan pg_index to find those that have
    + * indisclustered set; if it was not given, scan pg_class and return all
    + * tables.
    + *
    + * Return it as a list of RelToCluster in the given memory context.
      */
     static List *
    -get_tables_to_cluster(MemoryContext cluster_context)
    +get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext per=
    mcxt)
     {
    -	Relation	indRelation;
    +	Relation	catalog;
     	TableScanDesc scan;
    -	ScanKeyData entry;
    -	HeapTuple	indexTuple;
    -	Form_pg_index index;
    -	MemoryContext old_context;
    +	HeapTuple	tuple;
     	List	   *rtcs =3D NIL;
    =20
    -	/*
    -	 * Get all indexes that have indisclustered set and that the current user
    -	 * has the appropriate privileges for.
    -	 */
    -	indRelation =3D table_open(IndexRelationId, AccessShareLock);
    -	ScanKeyInit(&entry,
    -				Anum_pg_index_indisclustered,
    -				BTEqualStrategyNumber, F_BOOLEQ,
    -				BoolGetDatum(true));
    -	scan =3D table_beginscan_catalog(indRelation, 1, &entry);
    -	while ((indexTuple =3D heap_getnext(scan, ForwardScanDirection)) !=3D NUL=
    L)
    +	if (usingindex)
     	{
    -		RelToCluster *rtc;
    +		ScanKeyData entry;
    +
    +		catalog =3D table_open(IndexRelationId, AccessShareLock);
    +		ScanKeyInit(&entry,
    +					Anum_pg_index_indisclustered,
    +					BTEqualStrategyNumber, F_BOOLEQ,
    +					BoolGetDatum(true));
    +		scan =3D table_beginscan_catalog(catalog, 1, &entry);
    +		while ((tuple =3D heap_getnext(scan, ForwardScanDirection)) !=3D NULL)
    +		{
    +			RelToCluster *rtc;
    +			Form_pg_index index;
    +			MemoryContext oldcxt;
    =20
    -		index =3D (Form_pg_index) GETSTRUCT(indexTuple);
    +			index =3D (Form_pg_index) GETSTRUCT(tuple);
    =20
    -		if (!cluster_is_permitted_for_relation(index->indrelid, GetUserId()))
    -			continue;
    +			/*
    +			 * Try to obtain a light lock on the index's table, to ensure it
    +			 * doesn't go away while we collect the list.  If we cannot, just
    +			 * disregard it.
    +			 */
    +			if (!ConditionalLockRelationOid(index->indrelid, AccessShareLock))
    +				continue;
    =20
    -		/* Use a permanent memory context for the result list */
    -		old_context =3D MemoryContextSwitchTo(cluster_context);
    +			/* Verify that the table still exists */
    +			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(index->indrelid)))
    +			{
    +				/* Release useless lock */
    +				UnlockRelationOid(index->indrelid, AccessShareLock);
    +				continue;
    +			}
    =20
    -		rtc =3D (RelToCluster *) palloc(sizeof(RelToCluster));
    -		rtc->tableOid =3D index->indrelid;
    -		rtc->indexOid =3D index->indexrelid;
    -		rtcs =3D lappend(rtcs, rtc);
    +			if (!cluster_is_permitted_for_relation(cmd, index->indrelid,
    +												   GetUserId()))
    +				continue;
    +
    +			/* Use a permanent memory context for the result list */
    +			oldcxt =3D MemoryContextSwitchTo(permcxt);
    +			rtc =3D palloc(sizeof(RelToCluster));
    +			rtc->tableOid =3D index->indrelid;
    +			rtc->indexOid =3D index->indexrelid;
    +			rtcs =3D lappend(rtcs, rtc);
    +			MemoryContextSwitchTo(oldcxt);
    +		}
    +	}
    +	else
    +	{
    +		catalog =3D table_open(RelationRelationId, AccessShareLock);
    +		scan =3D table_beginscan_catalog(catalog, 0, NULL);
    +
    +		while ((tuple =3D heap_getnext(scan, ForwardScanDirection)) !=3D NULL)
    +		{
    +			RelToCluster *rtc;
    +			Form_pg_class class;
    +			MemoryContext oldcxt;
    +
    +			class =3D (Form_pg_class) GETSTRUCT(tuple);
    +
    +			/*
    +			 * Try to obtain a light lock on the table, to ensure it doesn't
    +			 * go away while we collect the list.  If we cannot, just
    +			 * disregard the table.
    +			 */
    +			if (!ConditionalLockRelationOid(class->oid, AccessShareLock))
    +				continue;
    =20
    -		MemoryContextSwitchTo(old_context);
    +			/* Verify that the table still exists */
    +			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(class->oid)))
    +			{
    +				/* Release useless lock */
    +				UnlockRelationOid(class->oid, AccessShareLock);
    +				continue;
    +			}
    +
    +			/* Can only process plain tables and matviews */
    +			if (class->relkind !=3D RELKIND_RELATION &&
    +				class->relkind !=3D RELKIND_MATVIEW)
    +				continue;
    +
    +			/* noisily skip rels which the user can't process */
    +			if (!cluster_is_permitted_for_relation(cmd, class->oid,
    +												   GetUserId()))
    +				continue;
    +
    +			/* Use a permanent memory context for the result list */
    +			oldcxt =3D MemoryContextSwitchTo(permcxt);
    +			rtc =3D palloc(sizeof(RelToCluster));
    +			rtc->tableOid =3D class->oid;
    +			rtc->indexOid =3D InvalidOid;
    +			rtcs =3D lappend(rtcs, rtc);
    +			MemoryContextSwitchTo(oldcxt);
    +		}
     	}
    -	table_endscan(scan);
    =20
    -	relation_close(indRelation, AccessShareLock);
    +	table_endscan(scan);
    +	relation_close(catalog, AccessShareLock);
    =20
     	return rtcs;
     }
    =20
     /*
    - * Given an index on a partitioned table, return a list of RelToCluster for
    + * Given a partitioned table or its index, return a list of RelToCluster f=
    or
      * all the children leaves tables/indexes.
      *
      * Like expand_vacuum_rel, but here caller must hold AccessExclusiveLock
      * on the table containing the index.
    + *
    + * 'rel_is_index' tells whether 'relid' is that of an index (true) or of t=
    he
    + * owning relation.
      */
     static List *
    -get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid index=
    Oid)
    +get_tables_to_repack_partitioned(RepackCommand cmd, Oid relid,
    +								 bool rel_is_index, MemoryContext permcxt)
     {
     	List	   *inhoids;
    -	ListCell   *lc;
     	List	   *rtcs =3D NIL;
    -	MemoryContext old_context;
    =20
    -	/* Do not lock the children until they're processed */
    -	inhoids =3D find_all_inheritors(indexOid, NoLock, NULL);
    -
    -	foreach(lc, inhoids)
    +	/*
    +	 * Do not lock the children until they're processed.  Note that we do hold
    +	 * a lock on the parent partitioned table.
    +	 */
    +	inhoids =3D find_all_inheritors(relid, NoLock, NULL);
    +	foreach_oid(child_oid, inhoids)
     	{
    -		Oid			indexrelid =3D lfirst_oid(lc);
    -		Oid			relid =3D IndexGetRelation(indexrelid, false);
    +		Oid			table_oid,
    +					index_oid;
     		RelToCluster *rtc;
    +		MemoryContext oldcxt;
    =20
    -		/* consider only leaf indexes */
    -		if (get_rel_relkind(indexrelid) !=3D RELKIND_INDEX)
    -			continue;
    +		if (rel_is_index)
    +		{
    +			/* consider only leaf indexes */
    +			if (get_rel_relkind(child_oid) !=3D RELKIND_INDEX)
    +				continue;
    +
    +			table_oid =3D IndexGetRelation(child_oid, false);
    +			index_oid =3D child_oid;
    +		}
    +		else
    +		{
    +			/* consider only leaf relations */
    +			if (get_rel_relkind(child_oid) !=3D RELKIND_RELATION)
    +				continue;
    +
    +			table_oid =3D child_oid;
    +			index_oid =3D InvalidOid;
    +		}
    =20
     		/*
     		 * It's possible that the user does not have privileges to CLUSTER the
    -		 * leaf partition despite having such privileges on the partitioned
    -		 * table.  We skip any partitions which the user is not permitted to
    -		 * CLUSTER.
    +		 * leaf partition despite having them on the partitioned table.  Skip
    +		 * if so.
     		 */
    -		if (!cluster_is_permitted_for_relation(relid, GetUserId()))
    +		if (!cluster_is_permitted_for_relation(cmd, table_oid, GetUserId()))
     			continue;
    =20
     		/* Use a permanent memory context for the result list */
    -		old_context =3D MemoryContextSwitchTo(cluster_context);
    -
    -		rtc =3D (RelToCluster *) palloc(sizeof(RelToCluster));
    -		rtc->tableOid =3D relid;
    -		rtc->indexOid =3D indexrelid;
    +		oldcxt =3D MemoryContextSwitchTo(permcxt);
    +		rtc =3D palloc(sizeof(RelToCluster));
    +		rtc->tableOid =3D table_oid;
    +		rtc->indexOid =3D index_oid;
     		rtcs =3D lappend(rtcs, rtc);
    -
    -		MemoryContextSwitchTo(old_context);
    +		MemoryContextSwitchTo(oldcxt);
     	}
    =20
     	return rtcs;
    @@ -1742,13 +1831,167 @@ get_tables_to_cluster_partitioned(MemoryContext cl=
    uster_context, Oid indexOid)
      * function emits a WARNING.
      */
     static bool
    -cluster_is_permitted_for_relation(Oid relid, Oid userid)
    +cluster_is_permitted_for_relation(RepackCommand cmd, Oid relid, Oid userid)
     {
    +	Assert(cmd =3D=3D REPACK_COMMAND_CLUSTER || cmd =3D=3D REPACK_COMMAND_REP=
    ACK);
    +
     	if (pg_class_aclcheck(relid, userid, ACL_MAINTAIN) =3D=3D ACLCHECK_OK)
     		return true;
    =20
     	ereport(WARNING,
    -			(errmsg("permission denied to cluster \"%s\", skipping it",
    -					get_rel_name(relid))));
    +			errmsg("permission denied to execute %s on \"%s\", skipping it",
    +				   RepackCommandAsString(cmd),
    +				   get_rel_name(relid)));
    +
     	return false;
     }
    +
    +
    +/*
    + * Given a RepackStmt with an indicated relation name, resolve the relation
    + * name, obtain lock on it, then determine what to do based on the relation
    + * type: if it's table and not partitioned, repack it as indicated (using =
    an
    + * existing clustered index, or following the given one), and return NULL.
    + *
    + * On the other hand, if the table is partitioned, do nothing further and
    + * instead return the opened and locked relcache entry, so that caller can
    + * process the partitions using the multiple-table handling code.  In this
    + * case, if an index name is given, it's up to the caller to resolve it.
    + */
    +static Relation
    +process_single_relation(RepackStmt *stmt, ClusterParams *params)
    +{
    +	Relation	rel;
    +	Oid			tableOid;
    +
    +	Assert(stmt->relation !=3D NULL);
    +	Assert(stmt->command =3D=3D REPACK_COMMAND_CLUSTER ||
    +		   stmt->command =3D=3D REPACK_COMMAND_REPACK);
    +
    +	/*
    +	 * Find, lock, and check permissions on the table.  We obtain
    +	 * AccessExclusiveLock right away to avoid lock-upgrade hazard in the
    +	 * single-transaction case.
    +	 */
    +	tableOid =3D RangeVarGetRelidExtended(stmt->relation->relation,
    +										AccessExclusiveLock,
    +										0,
    +										RangeVarCallbackMaintainsTable,
    +										NULL);
    +	rel =3D table_open(tableOid, NoLock);
    +
    +	/*
    +	 * Reject clustering a remote temp table ... their local buffer manager is
    +	 * not going to cope.
    +	 */
    +	if (RELATION_IS_OTHER_TEMP(rel))
    +		ereport(ERROR,
    +				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    +				errmsg("cannot execute %s on temporary tables of other sessions",
    +					   RepackCommandAsString(stmt->command)));
    +
    +	/*
    +	 * Make sure ANALYZE is specified if a column list is present.
    +	 */
    +	if ((params->options & CLUOPT_ANALYZE) =3D=3D 0 && stmt->relation->va_col=
    s !=3D NIL)
    +		ereport(ERROR,
    +				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    +				errmsg("ANALYZE option must be specified when a column list is provide=
    d"));
    +
    +	/*
    +	 * For partitioned tables, let caller handle this.  Otherwise, process it
    +	 * here and we're done.
    +	 */
    +	if (rel->rd_rel->relkind =3D=3D RELKIND_PARTITIONED_TABLE)
    +		return rel;
    +	else
    +	{
    +		Oid			indexOid;
    +
    +		indexOid =3D determine_clustered_index(rel, stmt->usingindex,
    +											 stmt->indexname);
    +		if (OidIsValid(indexOid))
    +			check_index_is_clusterable(rel, indexOid, AccessExclusiveLock);
    +		cluster_rel(stmt->command, rel, indexOid, params);
    +
    +		/* Do an analyze, if requested */
    +		if (params->options & CLUOPT_ANALYZE)
    +		{
    +			VacuumParams vac_params =3D {0};
    +
    +			vac_params.options |=3D VACOPT_ANALYZE;
    +			if (params->options & CLUOPT_VERBOSE)
    +				vac_params.options |=3D VACOPT_VERBOSE;
    +			analyze_rel(RelationGetRelid(rel), NULL, vac_params,
    +						stmt->relation->va_cols, true, NULL);
    +		}
    +
    +		return NULL;
    +	}
    +}
    +
    +/*
    + * Given a relation and the usingindex/indexname options in a
    + * REPACK USING INDEX or CLUSTER command, return the OID of the
    + * index to use for clustering the table.
    + *
    + * Caller must hold lock on the relation so that the set of indexes
    + * doesn't change, and must call check_index_is_clusterable.
    + */
    +static Oid
    +determine_clustered_index(Relation rel, bool usingindex, const char *index=
    name)
    +{
    +	Oid			indexOid;
    +
    +	if (indexname =3D=3D NULL && usingindex)
    +	{
    +		/*
    +		 * If USING INDEX with no name is given, find a clustered index, or
    +		 * error out if none.
    +		 */
    +		indexOid =3D InvalidOid;
    +		foreach_oid(idxoid, RelationGetIndexList(rel))
    +		{
    +			if (get_index_isclustered(idxoid))
    +			{
    +				indexOid =3D idxoid;
    +				break;
    +			}
    +		}
    +
    +		if (!OidIsValid(indexOid))
    +			ereport(ERROR,
    +					errcode(ERRCODE_UNDEFINED_OBJECT),
    +					errmsg("there is no previously clustered index for table \"%s\"",
    +						   RelationGetRelationName(rel)));
    +	}
    +	else if (indexname !=3D NULL)
    +	{
    +		/* An index was specified; obtain its OID. */
    +		indexOid =3D get_relname_relid(indexname, rel->rd_rel->relnamespace);
    +		if (!OidIsValid(indexOid))
    +			ereport(ERROR,
    +					errcode(ERRCODE_UNDEFINED_OBJECT),
    +					errmsg("index \"%s\" for table \"%s\" does not exist",
    +						   indexname, RelationGetRelationName(rel)));
    +	}
    +	else
    +		indexOid =3D InvalidOid;
    +
    +	return indexOid;
    +}
    +
    +static const char *
    +RepackCommandAsString(RepackCommand cmd)
    +{
    +	switch (cmd)
    +	{
    +		case REPACK_COMMAND_REPACK:
    +			return "REPACK";
    +		case REPACK_COMMAND_VACUUMFULL:
    +			return "VACUUM";
    +		case REPACK_COMMAND_CLUSTER:
    +			return "CLUSTER";
    +	}
    +	return "???";
    +}
    diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
    index e785dd55ce5..827e66724b5 100644
    --- a/src/backend/commands/vacuum.c
    +++ b/src/backend/commands/vacuum.c
    @@ -349,7 +349,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, boo=
    l isTopLevel)
     		}
     	}
    =20
    -
     	/*
     	 * Sanity check DISABLE_PAGE_SKIPPING option.
     	 */
    @@ -2287,8 +2286,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParam=
    s params,
     			if ((params.options & VACOPT_VERBOSE) !=3D 0)
     				cluster_params.options |=3D CLUOPT_VERBOSE;
    =20
    -			/* VACUUM FULL is now a variant of CLUSTER; see cluster.c */
    -			cluster_rel(rel, InvalidOid, &cluster_params);
    +			/* VACUUM FULL is a variant of REPACK; see cluster.c */
    +			cluster_rel(REPACK_COMMAND_VACUUMFULL, rel, InvalidOid,
    +						&cluster_params);
     			/* cluster_rel closes the relation, but keeps lock */
    =20
     			rel =3D NULL;
    diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
    index c3a0a354a9c..c314c11e23d 100644
    --- a/src/backend/parser/gram.y
    +++ b/src/backend/parser/gram.y
    @@ -286,7 +286,7 @@ static Node *makeRecursiveViewSelect(char *relname, Lis=
    t *aliases, Node *query);
     		AlterCompositeTypeStmt AlterUserMappingStmt
     		AlterRoleStmt AlterRoleSetStmt AlterPolicyStmt AlterStatsStmt
     		AlterDefaultPrivilegesStmt DefACLAction
    -		AnalyzeStmt CallStmt ClosePortalStmt ClusterStmt CommentStmt
    +		AnalyzeStmt CallStmt ClosePortalStmt CommentStmt
     		ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt
     		CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt
     		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
    @@ -303,7 +303,7 @@ static Node *makeRecursiveViewSelect(char *relname, Lis=
    t *aliases, Node *query);
     		GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt
     		ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt Prepar=
    ableStmt
     		CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt
    -		RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRol=
    eStmt
    +		RemoveFuncStmt RemoveOperStmt RenameStmt RepackStmt ReturnStmt RevokeStm=
    t RevokeRoleStmt
     		RuleActionStmt RuleActionStmtOrEmpty RuleStmt
     		SecLabelStmt SelectStmt TransactionStmt TransactionStmtLegacy TruncateSt=
    mt
     		UnlistenStmt UpdateStmt VacuumStmt
    @@ -322,7 +322,7 @@ static Node *makeRecursiveViewSelect(char *relname, Lis=
    t *aliases, Node *query);
    =20
     %type <str>			opt_single_name
     %type <list>		opt_qualified_name
    -%type <boolean>		opt_concurrently
    +%type <boolean>		opt_concurrently opt_usingindex
     %type <dbehavior>	opt_drop_behavior
     %type <list>		opt_utility_option_list
     %type <list>		opt_wait_with_clause
    @@ -770,7 +770,7 @@ static Node *makeRecursiveViewSelect(char *relname, Lis=
    t *aliases, Node *query);
     	QUOTE QUOTES
    =20
     	RANGE READ REAL REASSIGN RECURSIVE REF_P REFERENCES REFERENCING
    -	REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA
    +	REFRESH REINDEX RELATIVE_P RELEASE RENAME REPACK REPEATABLE REPLACE REPLI=
    CA
     	RESET RESPECT_P RESTART RESTRICT RETURN RETURNING RETURNS REVOKE RIGHT RO=
    LE ROLLBACK ROLLUP
     	ROUTINE ROUTINES ROW ROWS RULE
    =20
    @@ -1032,7 +1032,6 @@ stmt:
     			| CallStmt
     			| CheckPointStmt
     			| ClosePortalStmt
    -			| ClusterStmt
     			| CommentStmt
     			| ConstraintsSetStmt
     			| CopyStmt
    @@ -1106,6 +1105,7 @@ stmt:
     			| RemoveFuncStmt
     			| RemoveOperStmt
     			| RenameStmt
    +			| RepackStmt
     			| RevokeStmt
     			| RevokeRoleStmt
     			| RuleStmt
    @@ -1143,6 +1143,11 @@ opt_concurrently:
     			| /*EMPTY*/						{ $$ =3D false; }
     		;
    =20
    +opt_usingindex:
    +			USING INDEX						{ $$ =3D true; }
    +			| /* EMPTY */					{ $$ =3D false; }
    +		;
    +
     opt_drop_behavior:
     			CASCADE							{ $$ =3D DROP_CASCADE; }
     			| RESTRICT						{ $$ =3D DROP_RESTRICT; }
    @@ -11979,38 +11984,82 @@ CreateConversionStmt:
     /*************************************************************************=
    ****
      *
      *		QUERY:
    + *				REPACK [ (options) ] [ <qualified_name> [ <name_list> ] [ USING INDE=
    X <index_name> ] ]
    + *
    + *			obsolete variants:
      *				CLUSTER (options) [ <qualified_name> [ USING <index_name> ] ]
      *				CLUSTER [VERBOSE] [ <qualified_name> [ USING <index_name> ] ]
      *				CLUSTER [VERBOSE] <index_name> ON <qualified_name> (for pre-8.3)
      *
      *************************************************************************=
    ****/
    =20
    -ClusterStmt:
    -			CLUSTER '(' utility_option_list ')' qualified_name cluster_index_specif=
    ication
    +RepackStmt:
    +			REPACK opt_utility_option_list vacuum_relation USING INDEX name
     				{
    -					ClusterStmt *n =3D makeNode(ClusterStmt);
    +					RepackStmt *n =3D makeNode(RepackStmt);
    =20
    -					n->relation =3D $5;
    +					n->command =3D REPACK_COMMAND_REPACK;
    +					n->relation =3D (VacuumRelation *) $3;
     					n->indexname =3D $6;
    +					n->usingindex =3D true;
    +					n->params =3D $2;
    +					$$ =3D (Node *) n;
    +				}
    +			| REPACK opt_utility_option_list vacuum_relation opt_usingindex
    +				{
    +					RepackStmt *n =3D makeNode(RepackStmt);
    +
    +					n->command =3D REPACK_COMMAND_REPACK;
    +					n->relation =3D (VacuumRelation *) $3;
    +					n->indexname =3D NULL;
    +					n->usingindex =3D $4;
    +					n->params =3D $2;
    +					$$ =3D (Node *) n;
    +				}
    +			| REPACK opt_utility_option_list opt_usingindex
    +				{
    +					RepackStmt *n =3D makeNode(RepackStmt);
    +
    +					n->command =3D REPACK_COMMAND_REPACK;
    +					n->relation =3D NULL;
    +					n->indexname =3D NULL;
    +					n->usingindex =3D $3;
    +					n->params =3D $2;
    +					$$ =3D (Node *) n;
    +				}
    +			| CLUSTER '(' utility_option_list ')' qualified_name cluster_index_spec=
    ification
    +				{
    +					RepackStmt *n =3D makeNode(RepackStmt);
    +
    +					n->command =3D REPACK_COMMAND_CLUSTER;
    +					n->relation =3D makeNode(VacuumRelation);
    +					n->relation->relation =3D $5;
    +					n->indexname =3D $6;
    +					n->usingindex =3D true;
     					n->params =3D $3;
     					$$ =3D (Node *) n;
     				}
     			| CLUSTER opt_utility_option_list
     				{
    -					ClusterStmt *n =3D makeNode(ClusterStmt);
    +					RepackStmt *n =3D makeNode(RepackStmt);
    =20
    +					n->command =3D REPACK_COMMAND_CLUSTER;
     					n->relation =3D NULL;
     					n->indexname =3D NULL;
    +					n->usingindex =3D true;
     					n->params =3D $2;
     					$$ =3D (Node *) n;
     				}
     			/* unparenthesized VERBOSE kept for pre-14 compatibility */
     			| CLUSTER opt_verbose qualified_name cluster_index_specification
     				{
    -					ClusterStmt *n =3D makeNode(ClusterStmt);
    +					RepackStmt *n =3D makeNode(RepackStmt);
    =20
    -					n->relation =3D $3;
    +					n->command =3D REPACK_COMMAND_CLUSTER;
    +					n->relation =3D makeNode(VacuumRelation);
    +					n->relation->relation =3D $3;
     					n->indexname =3D $4;
    +					n->usingindex =3D true;
     					if ($2)
     						n->params =3D list_make1(makeDefElem("verbose", NULL, @2));
     					$$ =3D (Node *) n;
    @@ -12018,20 +12067,25 @@ ClusterStmt:
     			/* unparenthesized VERBOSE kept for pre-17 compatibility */
     			| CLUSTER VERBOSE
     				{
    -					ClusterStmt *n =3D makeNode(ClusterStmt);
    +					RepackStmt *n =3D makeNode(RepackStmt);
    =20
    +					n->command =3D REPACK_COMMAND_CLUSTER;
     					n->relation =3D NULL;
     					n->indexname =3D NULL;
    +					n->usingindex =3D true;
     					n->params =3D list_make1(makeDefElem("verbose", NULL, @2));
     					$$ =3D (Node *) n;
     				}
     			/* kept for pre-8.3 compatibility */
     			| CLUSTER opt_verbose name ON qualified_name
     				{
    -					ClusterStmt *n =3D makeNode(ClusterStmt);
    +					RepackStmt *n =3D makeNode(RepackStmt);
    =20
    -					n->relation =3D $5;
    +					n->command =3D REPACK_COMMAND_CLUSTER;
    +					n->relation =3D makeNode(VacuumRelation);
    +					n->relation->relation =3D $5;
     					n->indexname =3D $3;
    +					n->usingindex =3D true;
     					if ($2)
     						n->params =3D list_make1(makeDefElem("verbose", NULL, @2));
     					$$ =3D (Node *) n;
    @@ -18069,6 +18123,7 @@ unreserved_keyword:
     			| RELATIVE_P
     			| RELEASE
     			| RENAME
    +			| REPACK
     			| REPEATABLE
     			| REPLACE
     			| REPLICA
    @@ -18704,6 +18759,7 @@ bare_label_keyword:
     			| RELATIVE_P
     			| RELEASE
     			| RENAME
    +			| REPACK
     			| REPEATABLE
     			| REPLACE
     			| REPLICA
    diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
    index d18a3a60a46..d01895d1cf1 100644
    --- a/src/backend/tcop/utility.c
    +++ b/src/backend/tcop/utility.c
    @@ -279,9 +279,9 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
     				return COMMAND_OK_IN_RECOVERY | COMMAND_OK_IN_READ_ONLY_TXN;
     			}
    =20
    -		case T_ClusterStmt:
     		case T_ReindexStmt:
     		case T_VacuumStmt:
    +		case T_RepackStmt:
     			{
     				/*
     				 * These commands write WAL, so they're not strictly
    @@ -856,14 +856,14 @@ standard_ProcessUtility(PlannedStmt *pstmt,
     			ExecuteCallStmt(castNode(CallStmt, parsetree), params, isAtomicContext,=
     dest);
     			break;
    =20
    -		case T_ClusterStmt:
    -			cluster(pstate, (ClusterStmt *) parsetree, isTopLevel);
    -			break;
    -
     		case T_VacuumStmt:
     			ExecVacuum(pstate, (VacuumStmt *) parsetree, isTopLevel);
     			break;
    =20
    +		case T_RepackStmt:
    +			ExecRepack(pstate, (RepackStmt *) parsetree, isTopLevel);
    +			break;
    +
     		case T_ExplainStmt:
     			ExplainQuery(pstate, (ExplainStmt *) parsetree, params, dest);
     			break;
    @@ -2864,10 +2864,6 @@ CreateCommandTag(Node *parsetree)
     			tag =3D CMDTAG_CALL;
     			break;
    =20
    -		case T_ClusterStmt:
    -			tag =3D CMDTAG_CLUSTER;
    -			break;
    -
     		case T_VacuumStmt:
     			if (((VacuumStmt *) parsetree)->is_vacuumcmd)
     				tag =3D CMDTAG_VACUUM;
    @@ -2875,6 +2871,10 @@ CreateCommandTag(Node *parsetree)
     				tag =3D CMDTAG_ANALYZE;
     			break;
    =20
    +		case T_RepackStmt:
    +			tag =3D CMDTAG_REPACK;
    +			break;
    +
     		case T_ExplainStmt:
     			tag =3D CMDTAG_EXPLAIN;
     			break;
    @@ -3516,7 +3516,7 @@ GetCommandLogLevel(Node *parsetree)
     			lev =3D LOGSTMT_ALL;
     			break;
    =20
    -		case T_ClusterStmt:
    +		case T_RepackStmt:
     			lev =3D LOGSTMT_DDL;
     			break;
    =20
    diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pg=
    statfuncs.c
    index 7e2ed69138a..1a82534e347 100644
    --- a/src/backend/utils/adt/pgstatfuncs.c
    +++ b/src/backend/utils/adt/pgstatfuncs.c
    @@ -289,6 +289,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
     		cmdtype =3D PROGRESS_COMMAND_ANALYZE;
     	else if (pg_strcasecmp(cmd, "CLUSTER") =3D=3D 0)
     		cmdtype =3D PROGRESS_COMMAND_CLUSTER;
    +	else if (pg_strcasecmp(cmd, "REPACK") =3D=3D 0)
    +		cmdtype =3D PROGRESS_COMMAND_REPACK;
     	else if (pg_strcasecmp(cmd, "CREATE INDEX") =3D=3D 0)
     		cmdtype =3D PROGRESS_COMMAND_CREATE_INDEX;
     	else if (pg_strcasecmp(cmd, "BASEBACKUP") =3D=3D 0)
    diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
    index 20d7a65c614..626d9f1c98b 100644
    --- a/src/bin/psql/tab-complete.in.c
    +++ b/src/bin/psql/tab-complete.in.c
    @@ -1267,7 +1267,7 @@ static const char *const sql_commands[] =3D {
     	"DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN",
     	"FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD=
    ", "LOCK",
     	"MERGE INTO", "MOVE", "NOTIFY", "PREPARE",
    -	"REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE",
    +	"REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "REPACK",
     	"RESET", "REVOKE", "ROLLBACK",
     	"SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START",
     	"TABLE", "TRUNCATE", "UNLISTEN", "UPDATE", "VACUUM", "VALUES",
    @@ -5040,6 +5040,46 @@ match_previous_words(int pattern_id,
     			COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
     	}
    =20
    +/* REPACK */
    +	else if (Matches("REPACK"))
    +		COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_clusterables,
    +										"(", "USING INDEX");
    +	else if (Matches("REPACK", "(*)"))
    +		COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_clusterables,
    +										"USING INDEX");
    +	else if (Matches("REPACK", MatchAnyExcept("(")))
    +		COMPLETE_WITH("USING INDEX");
    +	else if (Matches("REPACK", "(*)", MatchAnyExcept("(")))
    +		COMPLETE_WITH("USING INDEX");
    +	else if (Matches("REPACK", MatchAny, "USING", "INDEX") ||
    +			 Matches("REPACK", "(*)", MatchAny, "USING", "INDEX"))
    +	{
    +		set_completion_reference(prev3_wd);
    +		COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
    +	}
    +	/*
    +	 * Complete ... [ (*) ] <sth> USING INDEX, with a list of indexes for
    +	 * <sth>.
    +	 */
    +	else if (TailMatches(MatchAny, "USING", "INDEX"))
    +	{
    +		set_completion_reference(prev3_wd);
    +		COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table);
    +	}
    +	else if (HeadMatches("REPACK", "(*") &&
    +			 !HeadMatches("REPACK", "(*)"))
    +	{
    +		/*
    +		 * This fires if we're in an unfinished parenthesized option list.
    +		 * get_previous_words treats a completed parenthesized option list as
    +		 * one word, so the above test is correct.
    +		 */
    +		if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
    +			COMPLETE_WITH("ANALYZE", "VERBOSE");
    +		else if (TailMatches("ANALYZE", "VERBOSE"))
    +			COMPLETE_WITH("ON", "OFF");
    +	}
    +
     /* SECURITY LABEL */
     	else if (Matches("SECURITY"))
     		COMPLETE_WITH("LABEL");
    diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
    index 019ca06455d..f0c1bd4175c 100644
    --- a/src/bin/scripts/Makefile
    +++ b/src/bin/scripts/Makefile
    @@ -16,7 +16,7 @@ subdir =3D src/bin/scripts
     top_builddir =3D ../../..
     include $(top_builddir)/src/Makefile.global
    =20
    -PROGRAMS =3D createdb createuser dropdb dropuser clusterdb vacuumdb reinde=
    xdb pg_isready
    +PROGRAMS =3D createdb createuser dropdb dropuser clusterdb vacuumdb reinde=
    xdb pg_isready pg_repackdb
    =20
     override CPPFLAGS :=3D -I$(libpq_srcdir) $(CPPFLAGS)
     LDFLAGS_INTERNAL +=3D -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_p=
    gport)
    @@ -31,6 +31,7 @@ clusterdb: clusterdb.o common.o $(WIN32RES) | submake-lib=
    pq submake-libpgport su
     vacuumdb: vacuumdb.o vacuuming.o common.o $(WIN32RES) | submake-libpq subm=
    ake-libpgport submake-libpgfeutils
     reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgp=
    ort submake-libpgfeutils
     pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libp=
    gport submake-libpgfeutils
    +pg_repackdb: pg_repackdb.o vacuuming.o common.o $(WIN32RES) | submake-libp=
    q submake-libpgport submake-libpgfeutils
    =20
     install: all installdirs
     	$(INSTALL_PROGRAM) createdb$(X)   '$(DESTDIR)$(bindir)'/createdb$(X)
    @@ -41,6 +42,7 @@ install: all installdirs
     	$(INSTALL_PROGRAM) vacuumdb$(X)   '$(DESTDIR)$(bindir)'/vacuumdb$(X)
     	$(INSTALL_PROGRAM) reindexdb$(X)  '$(DESTDIR)$(bindir)'/reindexdb$(X)
     	$(INSTALL_PROGRAM) pg_isready$(X) '$(DESTDIR)$(bindir)'/pg_isready$(X)
    +	$(INSTALL_PROGRAM) pg_repackdb$(X) '$(DESTDIR)$(bindir)'/pg_repackdb$(X)
    =20
     installdirs:
     	$(MKDIR_P) '$(DESTDIR)$(bindir)'
    diff --git a/src/bin/scripts/meson.build b/src/bin/scripts/meson.build
    index a4fed59d1c9..be573cae682 100644
    --- a/src/bin/scripts/meson.build
    +++ b/src/bin/scripts/meson.build
    @@ -42,6 +42,7 @@ vacuuming_common =3D static_library('libvacuuming_common',
    =20
     binaries =3D [
       'vacuumdb',
    +  'pg_repackdb',
     ]
     foreach binary : binaries
       binary_sources =3D files('@0@.c'.format(binary))
    @@ -80,6 +81,7 @@ tests +=3D {
           't/100_vacuumdb.pl',
           't/101_vacuumdb_all.pl',
           't/102_vacuumdb_stages.pl',
    +      't/103_repackdb.pl',
           't/200_connstr.pl',
         ],
       },
    diff --git a/src/bin/scripts/pg_repackdb.c b/src/bin/scripts/pg_repackdb.c
    new file mode 100644
    index 00000000000..1edfa34ed0f
    --- /dev/null
    +++ b/src/bin/scripts/pg_repackdb.c
    @@ -0,0 +1,242 @@
    +/*-------------------------------------------------------------------------
    + *
    + * pg_repackdb
    + *		An utility to run REPACK
    + *
    + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
    + * Portions Copyright (c) 1994, Regents of the University of California
    + *
    + * FIXME: this is missing a way to specify the index to use to repack one
    + * table, or whether to pass a WITH INDEX clause when multiple tables are
    + * used.  Something like --index[=3Dindexname].  Adding that bleeds into
    + * vacuuming.c as well.
    + *
    + * src/bin/scripts/pg_repackdb.c
    + *
    + *-------------------------------------------------------------------------
    + */
    +
    +#include "postgres_fe.h"
    +
    +#include <limits.h>
    +
    +#include "common.h"
    +#include "common/logging.h"
    +#include "fe_utils/option_utils.h"
    +#include "vacuuming.h"
    +
    +static void help(const char *progname);
    +static void check_objfilter(bits32 objfilter);
    +
    +int
    +main(int argc, char *argv[])
    +{
    +	static struct option long_options[] =3D {
    +		{"host", required_argument, NULL, 'h'},
    +		{"port", required_argument, NULL, 'p'},
    +		{"username", required_argument, NULL, 'U'},
    +		{"no-password", no_argument, NULL, 'w'},
    +		{"password", no_argument, NULL, 'W'},
    +		{"echo", no_argument, NULL, 'e'},
    +		{"quiet", no_argument, NULL, 'q'},
    +		{"dbname", required_argument, NULL, 'd'},
    +		{"analyze", no_argument, NULL, 'z'},
    +		{"all", no_argument, NULL, 'a'},
    +		/* XXX this could be 'i', but optional_arg is messy */
    +		{"index", optional_argument, NULL, 1},
    +		{"table", required_argument, NULL, 't'},
    +		{"verbose", no_argument, NULL, 'v'},
    +		{"jobs", required_argument, NULL, 'j'},
    +		{"schema", required_argument, NULL, 'n'},
    +		{"exclude-schema", required_argument, NULL, 'N'},
    +		{"maintenance-db", required_argument, NULL, 2},
    +		{NULL, 0, NULL, 0}
    +	};
    +
    +	const char *progname;
    +	int			optindex;
    +	int			c;
    +	const char *dbname =3D NULL;
    +	const char *maintenance_db =3D NULL;
    +	ConnParams	cparams;
    +	bool		echo =3D false;
    +	bool		quiet =3D false;
    +	vacuumingOptions vacopts;
    +	SimpleStringList objects =3D {NULL, NULL};
    +	int			concurrentCons =3D 1;
    +	int			tbl_count =3D 0;
    +	int			ret;
    +
    +	/* initialize options */
    +	memset(&vacopts, 0, sizeof(vacopts));
    +	vacopts.mode =3D MODE_REPACK;
    +
    +	/* the same for connection parameters */
    +	memset(&cparams, 0, sizeof(cparams));
    +	cparams.prompt_password =3D TRI_DEFAULT;
    +
    +	pg_logging_init(argv[0]);
    +	progname =3D get_progname(argv[0]);
    +	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pgscripts"));
    +
    +	handle_help_version_opts(argc, argv, progname, help);
    +
    +	while ((c =3D getopt_long(argc, argv, "ad:eh:j:n:N:p:qt:U:vwWz",
    +							long_options, &optindex)) !=3D -1)
    +	{
    +		switch (c)
    +		{
    +			case 'a':
    +				vacopts.objfilter |=3D OBJFILTER_ALL_DBS;
    +				break;
    +			case 'd':
    +				vacopts.objfilter |=3D OBJFILTER_DATABASE;
    +				dbname =3D pg_strdup(optarg);
    +				break;
    +			case 'e':
    +				echo =3D true;
    +				break;
    +			case 'h':
    +				cparams.pghost =3D pg_strdup(optarg);
    +				break;
    +			case 'j':
    +				if (!option_parse_int(optarg, "-j/--jobs", 1, INT_MAX,
    +									  &concurrentCons))
    +					exit(1);
    +				break;
    +			case 'n':
    +				vacopts.objfilter |=3D OBJFILTER_SCHEMA;
    +				simple_string_list_append(&objects, optarg);
    +				break;
    +			case 'N':
    +				vacopts.objfilter |=3D OBJFILTER_SCHEMA_EXCLUDE;
    +				simple_string_list_append(&objects, optarg);
    +				break;
    +			case 'p':
    +				cparams.pgport =3D pg_strdup(optarg);
    +				break;
    +			case 'q':
    +				quiet =3D true;
    +				break;
    +			case 't':
    +				vacopts.objfilter |=3D OBJFILTER_TABLE;
    +				simple_string_list_append(&objects, optarg);
    +				tbl_count++;
    +				break;
    +			case 'U':
    +				cparams.pguser =3D pg_strdup(optarg);
    +				break;
    +			case 'v':
    +				vacopts.verbose =3D true;
    +				break;
    +			case 'w':
    +				cparams.prompt_password =3D TRI_NO;
    +				break;
    +			case 'W':
    +				cparams.prompt_password =3D TRI_YES;
    +				break;
    +			case 'z':
    +				vacopts.and_analyze =3D true;
    +				break;
    +			case 1:
    +				vacopts.using_index =3D true;
    +				if (optarg)
    +					vacopts.indexname =3D pg_strdup(optarg);
    +				else
    +					vacopts.indexname =3D NULL;
    +				break;
    +			case 2:
    +				maintenance_db =3D pg_strdup(optarg);
    +				break;
    +			default:
    +				/* getopt_long already emitted a complaint */
    +				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
    +				exit(1);
    +		}
    +	}
    +
    +	/*
    +	 * Non-option argument specifies database name as long as it wasn't
    +	 * already specified with -d / --dbname
    +	 */
    +	if (optind < argc && dbname =3D=3D NULL)
    +	{
    +		vacopts.objfilter |=3D OBJFILTER_DATABASE;
    +		dbname =3D argv[optind];
    +		optind++;
    +	}
    +
    +	if (optind < argc)
    +	{
    +		pg_log_error("too many command-line arguments (first is \"%s\")",
    +					 argv[optind]);
    +		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
    +		exit(1);
    +	}
    +
    +	/*
    +	 * Validate the combination of filters specified in the command-line
    +	 * options.
    +	 */
    +	check_objfilter(vacopts.objfilter);
    +
    +	ret =3D vacuuming_main(&cparams, dbname, maintenance_db, &vacopts,
    +						 &objects, tbl_count, concurrentCons,
    +						 progname, echo, quiet);
    +	exit(ret);
    +}
    +
    +/*
    + * Verify that the filters used at command line are compatible.
    + */
    +void
    +check_objfilter(bits32 objfilter)
    +{
    +	if ((objfilter & OBJFILTER_ALL_DBS) &&
    +		(objfilter & OBJFILTER_DATABASE))
    +		pg_fatal("cannot repack all databases and a specific one at the same tim=
    e");
    +
    +	if ((objfilter & OBJFILTER_TABLE) &&
    +		(objfilter & OBJFILTER_SCHEMA))
    +		pg_fatal("cannot repack all tables in schema(s) and specific table(s) at=
     the same time");
    +
    +	if ((objfilter & OBJFILTER_TABLE) &&
    +		(objfilter & OBJFILTER_SCHEMA_EXCLUDE))
    +		pg_fatal("cannot repack specific table(s) and exclude schema(s) at the s=
    ame time");
    +
    +	if ((objfilter & OBJFILTER_SCHEMA) &&
    +		(objfilter & OBJFILTER_SCHEMA_EXCLUDE))
    +		pg_fatal("cannot repack all tables in schema(s) and exclude schema(s) at=
     the same time");
    +}
    +
    +static void
    +help(const char *progname)
    +{
    +	printf(_("%s repacks a PostgreSQL database.\n\n"), progname);
    +	printf(_("Usage:\n"));
    +	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
    +	printf(_("\nOptions:\n"));
    +	printf(_("  -a, --all                       repack all databases\n"));
    +	printf(_("  -d, --dbname=3DDBNAME             database to repack\n"));
    +	printf(_("  -e, --echo                      show the commands being sent =
    to the server\n"));
    +	printf(_("      --index[=3DINDEX]             repack following an index\n=
    "));
    +	printf(_("  -j, --jobs=3DNUM                  use this many concurrent co=
    nnections to repack\n"));
    +	printf(_("  -n, --schema=3DSCHEMA             repack tables in the specif=
    ied schema(s) only\n"));
    +	printf(_("  -N, --exclude-schema=3DSCHEMA     do not repack tables in the=
     specified schema(s)\n"));
    +	printf(_("  -q, --quiet                     don't write any messages\n"));
    +	printf(_("  -t, --table=3D'TABLE[(COLUMNS)]'  repack specific table(s) on=
    ly\n"));
    +	printf(_("  -v, --verbose                   write a lot of output\n"));
    +	printf(_("  -V, --version                   output version information, t=
    hen exit\n"));
    +	printf(_("  -z, --analyze                   update optimizer statistics\n=
    "));
    +	printf(_("  -?, --help                      show this help, then exit\n")=
    );
    +	printf(_("\nConnection options:\n"));
    +	printf(_("  -h, --host=3DHOSTNAME       database server host or socket di=
    rectory\n"));
    +	printf(_("  -p, --port=3DPORT           database server port\n"));
    +	printf(_("  -U, --username=3DUSERNAME   user name to connect as\n"));
    +	printf(_("  -w, --no-password         never prompt for password\n"));
    +	printf(_("  -W, --password            force password prompt\n"));
    +	printf(_("  --maintenance-db=3DDBNAME   alternate maintenance database\n"=
    ));
    +	printf(_("\nRead the description of the SQL command REPACK for details.\n=
    "));
    +	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
    +	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
    +}
    diff --git a/src/bin/scripts/t/103_repackdb.pl b/src/bin/scripts/t/103_repa=
    ckdb.pl
    new file mode 100644
    index 00000000000..cadce9b837c
    --- /dev/null
    +++ b/src/bin/scripts/t/103_repackdb.pl
    @@ -0,0 +1,47 @@
    +# Copyright (c) 2021-2025, PostgreSQL Global Development Group
    +
    +use strict;
    +use warnings FATAL =3D> 'all';
    +
    +use PostgreSQL::Test::Cluster;
    +use PostgreSQL::Test::Utils;
    +use Test::More;
    +
    +program_help_ok('pg_repackdb');
    +program_version_ok('pg_repackdb');
    +program_options_handling_ok('pg_repackdb');
    +
    +my $node =3D PostgreSQL::Test::Cluster->new('main');
    +$node->init;
    +$node->start;
    +
    +$node->issues_sql_like(
    +	[ 'pg_repackdb', 'postgres', '-t', 'pg_class'],
    +	qr/statement: REPACK.*pg_class;/,
    +	'pg_repackdb processes a single table');
    +
    +$node->safe_psql('postgres', 'CREATE USER testusr;
    +	GRANT CREATE ON SCHEMA public TO testusr');
    +$node->safe_psql('postgres',
    +	'CREATE TABLE cluster_1 (a int primary key);
    +	ALTER TABLE cluster_1 CLUSTER ON cluster_1_pkey;
    +	CREATE TABLE cluster_2 (a int unique);
    +	ALTER TABLE cluster_2 CLUSTER ON cluster_2_a_key;',
    +	extra_params =3D> ['-U' =3D> 'testusr']);
    +
    +$node->issues_sql_like(
    +	[ 'pg_repackdb', 'postgres', '-U', 'testusr' ],
    +	qr/statement: REPACK.*;/,
    +	'SQL REPACK run');
    +
    +$node->issues_sql_like(
    +	[ 'pg_repackdb', 'postgres', '--index'],
    +	qr/statement: REPACK.*cluster_1 USING INDEX.*statement: REPACK.*cluster_2=
     USING INDEX/ms,
    +	'pg_repackdb --index chooses multiple tables');
    +
    +$node->issues_sql_like(
    +	[ 'pg_repackdb', 'postgres', '--analyze', '-t', 'cluster_1'],
    +	qr/statement: REPACK \(ANALYZE\) public.cluster_1/,
    +	'pg_repackdb --analyze works');
    +
    +done_testing();
    diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
    index f836f21fb03..d8d77cabe43 100644
    --- a/src/bin/scripts/vacuuming.c
    +++ b/src/bin/scripts/vacuuming.c
    @@ -1,6 +1,6 @@
     /*-------------------------------------------------------------------------
      * vacuuming.c
    - *		Helper routines for vacuumdb
    + *		Helper routines for vacuumdb and pg_repackdb
      *
      * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
      * Portions Copyright (c) 1994, Regents of the University of California
    @@ -43,8 +43,8 @@ static SimpleStringList *retrieve_objects(PGconn *conn,
     static void free_retrieved_objects(SimpleStringList *list);
     static void prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
     								   vacuumingOptions *vacopts, const char *table);
    -static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
    -							   const char *table);
    +static void run_vacuum_command(PGconn *conn, vacuumingOptions *vacopts,
    +							   const char *sql, bool echo, const char *table);
    =20
     /*
      * Executes vacuum/analyze as indicated.  Returns 0 if the plan is carried
    @@ -194,6 +194,14 @@ vacuum_one_database(ConnParams *cparams,
    =20
     	conn =3D connectDatabase(cparams, progname, echo, false, true);
    =20
    +	if (vacopts->mode =3D=3D MODE_REPACK && PQserverVersion(conn) < 190000)
    +	{
    +		/* XXX arguably, here we should use VACUUM FULL instead of failing */
    +		PQfinish(conn);
    +		pg_fatal("cannot use the \"%s\" command on server versions older than Po=
    stgreSQL %s",
    +				 "REPACK", "19");
    +	}
    +
     	if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600)
     	{
     		PQfinish(conn);
    @@ -286,9 +294,18 @@ vacuum_one_database(ConnParams *cparams,
     		if (vacopts->mode =3D=3D MODE_ANALYZE_IN_STAGES)
     			printf(_("%s: processing database \"%s\": %s\n"),
     				   progname, PQdb(conn), _(stage_messages[stage]));
    -		else
    +		else if (vacopts->mode =3D=3D MODE_ANALYZE)
    +			printf(_("%s: analyzing database \"%s\"\n"),
    +				   progname, PQdb(conn));
    +		else if (vacopts->mode =3D=3D MODE_VACUUM)
     			printf(_("%s: vacuuming database \"%s\"\n"),
     				   progname, PQdb(conn));
    +		else
    +		{
    +			Assert(vacopts->mode =3D=3D MODE_REPACK);
    +			printf(_("%s: repacking database \"%s\"\n"),
    +				   progname, PQdb(conn));
    +		}
     		fflush(stdout);
     	}
    =20
    @@ -383,7 +400,7 @@ vacuum_one_database(ConnParams *cparams,
     		 * through ParallelSlotsGetIdle.
     		 */
     		ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL);
    -		run_vacuum_command(free_slot->connection, sql.data,
    +		run_vacuum_command(free_slot->connection, vacopts, sql.data,
     						   echo, tabname);
    =20
     		cell =3D cell->next;
    @@ -408,7 +425,7 @@ vacuum_one_database(ConnParams *cparams,
     		}
    =20
     		ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL);
    -		run_vacuum_command(free_slot->connection, cmd, echo, NULL);
    +		run_vacuum_command(free_slot->connection, vacopts, cmd, echo, NULL);
    =20
     		if (!ParallelSlotsWaitCompletion(sa))
     			ret =3D EXIT_FAILURE; /* error already reported by handler */
    @@ -636,6 +653,35 @@ retrieve_objects(PGconn *conn, vacuumingOptions *vacop=
    ts,
     								 " AND listed_objects.object_oid IS NOT NULL\n");
     	}
    =20
    +	/*
    +	 * In REPACK mode, if the 'using_index' option was given but no index
    +	 * name, filter only tables that have an index with indisclustered set.
    +	 * (If an index name is given, we trust the user to pass a reasonable list
    +	 * of tables.)
    +	 *
    +	 * XXX it may be worth printing an error if an index name is given with no
    +	 * list of tables.
    +	 */
    +	if (vacopts->mode =3D=3D MODE_REPACK &&
    +		vacopts->using_index && !vacopts->indexname)
    +	{
    +		appendPQExpBufferStr(&catalog_query,
    +							 " AND EXISTS (SELECT 1 FROM pg_catalog.pg_index\n"
    +							 "    WHERE indrelid =3D c.oid AND indisclustered)\n");
    +	}
    +
    +	/*
    +	 * In REPACK mode, only consider the tables that the current user has
    +	 * MAINTAIN privileges on.  XXX maybe we should do this in all cases, not
    +	 * just REPACK.  The vacuumdb output is too noisy for no reason.
    +	 */
    +	if (vacopts->mode =3D=3D MODE_REPACK)
    +	{
    +		appendPQExpBufferStr(&catalog_query,
    +							 " AND pg_catalog.has_table_privilege(current_user, "
    +							 "c.oid, 'MAINTAIN')\n");
    +	}
    +
     	/*
     	 * If no tables were listed, filter for the relevant relation types.  If
     	 * tables were given via --table, don't bother filtering by relation type.
    @@ -874,8 +920,10 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
     			if (vacopts->verbose)
     				appendPQExpBufferStr(sql, " VERBOSE");
     		}
    +
    +		appendPQExpBuffer(sql, " %s", table);
     	}
    -	else
    +	else if (vacopts->mode =3D=3D MODE_VACUUM)
     	{
     		appendPQExpBufferStr(sql, "VACUUM");
    =20
    @@ -989,9 +1037,39 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
     			if (vacopts->and_analyze)
     				appendPQExpBufferStr(sql, " ANALYZE");
     		}
    +
    +		appendPQExpBuffer(sql, " %s", table);
     	}
    +	else if (vacopts->mode =3D=3D MODE_REPACK)
    +	{
    +		appendPQExpBufferStr(sql, "REPACK");
    =20
    -	appendPQExpBuffer(sql, " %s;", table);
    +		if (vacopts->verbose)
    +		{
    +			appendPQExpBuffer(sql, "%sVERBOSE", sep);
    +			sep =3D comma;
    +		}
    +		if (vacopts->and_analyze)
    +		{
    +			appendPQExpBuffer(sql, "%sANALYZE", sep);
    +			sep =3D comma;
    +		}
    +
    +		if (sep !=3D paren)
    +			appendPQExpBufferChar(sql, ')');
    +
    +		appendPQExpBuffer(sql, " %s", table);
    +
    +		if (vacopts->using_index)
    +		{
    +			appendPQExpBuffer(sql, " USING INDEX");
    +			if (vacopts->indexname)
    +				appendPQExpBuffer(sql, " %s", fmtIdEnc(vacopts->indexname,
    +													   PQclientEncoding(conn)));
    +		}
    +	}
    +
    +	appendPQExpBufferChar(sql, ';');
     }
    =20
     /*
    @@ -1001,8 +1079,8 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql,
      * Any errors during command execution are reported to stderr.
      */
     static void
    -run_vacuum_command(PGconn *conn, const char *sql, bool echo,
    -				   const char *table)
    +run_vacuum_command(PGconn *conn, vacuumingOptions *vacopts,
    +				   const char *sql, bool echo, const char *table)
     {
     	bool		status;
    =20
    @@ -1015,13 +1093,21 @@ run_vacuum_command(PGconn *conn, const char *sql, b=
    ool echo,
     	{
     		if (table)
     		{
    -			pg_log_error("vacuuming of table \"%s\" in database \"%s\" failed: %s",
    -						 table, PQdb(conn), PQerrorMessage(conn));
    +			if (vacopts->mode =3D=3D MODE_VACUUM)
    +				pg_log_error("vacuuming of table \"%s\" in database \"%s\" failed: %s",
    +							 table, PQdb(conn), PQerrorMessage(conn));
    +			else
    +				pg_log_error("repacking of table \"%s\" in database \"%s\" failed: %s",
    +							 table, PQdb(conn), PQerrorMessage(conn));
     		}
     		else
     		{
    -			pg_log_error("vacuuming of database \"%s\" failed: %s",
    -						 PQdb(conn), PQerrorMessage(conn));
    +			if (vacopts->mode =3D=3D MODE_VACUUM)
    +				pg_log_error("vacuuming of database \"%s\" failed: %s",
    +							 PQdb(conn), PQerrorMessage(conn));
    +			else
    +				pg_log_error("repacking of database \"%s\" failed: %s",
    +							 PQdb(conn), PQerrorMessage(conn));
     		}
     	}
     }
    diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h
    index 49f968b32e5..665dbaedfad 100644
    --- a/src/bin/scripts/vacuuming.h
    +++ b/src/bin/scripts/vacuuming.h
    @@ -20,6 +20,7 @@
     typedef enum
     {
     	MODE_VACUUM,
    +	MODE_REPACK,
     	MODE_ANALYZE,
     	MODE_ANALYZE_IN_STAGES
     } RunMode;
    @@ -37,6 +38,8 @@ typedef struct vacuumingOptions
     	bool		and_analyze;
     	bool		full;
     	bool		freeze;
    +	bool		using_index;
    +	char	   *indexname;
     	bool		disable_page_skipping;
     	bool		skip_locked;
     	int			min_xid_age;
    diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
    index 60088a64cbb..652542e8e65 100644
    --- a/src/include/commands/cluster.h
    +++ b/src/include/commands/cluster.h
    @@ -24,6 +24,7 @@
     #define CLUOPT_RECHECK 0x02		/* recheck relation state */
     #define CLUOPT_RECHECK_ISCLUSTERED 0x04 /* recheck relation state for
     										 * indisclustered */
    +#define CLUOPT_ANALYZE 0x08		/* do an ANALYZE */
    =20
     /* options for CLUSTER */
     typedef struct ClusterParams
    @@ -31,8 +32,11 @@ typedef struct ClusterParams
     	bits32		options;		/* bitmask of CLUOPT_* */
     } ClusterParams;
    =20
    -extern void cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel=
    );
    -extern void cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *par=
    ams);
    +
    +extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLev=
    el);
    +
    +extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid index=
    Oid,
    +						ClusterParams *params);
     extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
     									   LOCKMODE lockmode);
     extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_inter=
    nal);
    diff --git a/src/include/commands/progress.h b/src/include/commands/progres=
    s.h
    index 1cde4bd9bcf..ebf004b7aa5 100644
    --- a/src/include/commands/progress.h
    +++ b/src/include/commands/progress.h
    @@ -56,28 +56,34 @@
     #define PROGRESS_ANALYZE_PHASE_COMPUTE_EXT_STATS		4
     #define PROGRESS_ANALYZE_PHASE_FINALIZE_ANALYZE			5
    =20
    -/* Progress parameters for cluster */
    -#define PROGRESS_CLUSTER_COMMAND				0
    -#define PROGRESS_CLUSTER_PHASE					1
    -#define PROGRESS_CLUSTER_INDEX_RELID			2
    -#define PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED	3
    -#define PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN	4
    -#define PROGRESS_CLUSTER_TOTAL_HEAP_BLKS		5
    -#define PROGRESS_CLUSTER_HEAP_BLKS_SCANNED		6
    -#define PROGRESS_CLUSTER_INDEX_REBUILD_COUNT	7
    -
    -/* Phases of cluster (as advertised via PROGRESS_CLUSTER_PHASE) */
    -#define PROGRESS_CLUSTER_PHASE_SEQ_SCAN_HEAP	1
    -#define PROGRESS_CLUSTER_PHASE_INDEX_SCAN_HEAP	2
    -#define PROGRESS_CLUSTER_PHASE_SORT_TUPLES		3
    -#define PROGRESS_CLUSTER_PHASE_WRITE_NEW_HEAP	4
    -#define PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES	5
    -#define PROGRESS_CLUSTER_PHASE_REBUILD_INDEX	6
    -#define PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP	7
    -
    -/* Commands of PROGRESS_CLUSTER */
    -#define PROGRESS_CLUSTER_COMMAND_CLUSTER		1
    -#define PROGRESS_CLUSTER_COMMAND_VACUUM_FULL	2
    +/*
    + * Progress parameters for REPACK.
    + *
    + * Values for PROGRESS_REPACK_COMMAND are defined as in RepackCommand.
    + *
    + * Note: Since REPACK shares code with CLUSTER, these values are also
    + * used by CLUSTER. (CLUSTER is now deprecated, so it makes little sense to
    + * introduce a separate set of constants.)
    + */
    +#define PROGRESS_REPACK_COMMAND					0
    +#define PROGRESS_REPACK_PHASE					1
    +#define PROGRESS_REPACK_INDEX_RELID				2
    +#define PROGRESS_REPACK_HEAP_TUPLES_SCANNED		3
    +#define PROGRESS_REPACK_HEAP_TUPLES_WRITTEN		4
    +#define PROGRESS_REPACK_TOTAL_HEAP_BLKS			5
    +#define PROGRESS_REPACK_HEAP_BLKS_SCANNED		6
    +#define PROGRESS_REPACK_INDEX_REBUILD_COUNT		7
    +
    +/*
    + * Phases of repack (as advertised via PROGRESS_REPACK_PHASE).
    + */
    +#define PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP		1
    +#define PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP	2
    +#define PROGRESS_REPACK_PHASE_SORT_TUPLES		3
    +#define PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP	4
    +#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES	5
    +#define PROGRESS_REPACK_PHASE_REBUILD_INDEX		6
    +#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP		7
    =20
     /* Progress parameters for CREATE INDEX */
     /* 3, 4 and 5 reserved for "waitfor" metrics */
    diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
    index d14294a4ece..94892042b8d 100644
    --- a/src/include/nodes/parsenodes.h
    +++ b/src/include/nodes/parsenodes.h
    @@ -3951,18 +3951,6 @@ typedef struct AlterSystemStmt
     	VariableSetStmt *setstmt;	/* SET subcommand */
     } AlterSystemStmt;
    =20
    -/* ----------------------
    - *		Cluster Statement (support pbrown's cluster index implementation)
    - * ----------------------
    - */
    -typedef struct ClusterStmt
    -{
    -	NodeTag		type;
    -	RangeVar   *relation;		/* relation being indexed, or NULL if all */
    -	char	   *indexname;		/* original index defined */
    -	List	   *params;			/* list of DefElem nodes */
    -} ClusterStmt;
    -
     /* ----------------------
      *		Vacuum and Analyze Statements
      *
    @@ -3975,7 +3963,7 @@ typedef struct VacuumStmt
     	NodeTag		type;
     	List	   *options;		/* list of DefElem nodes */
     	List	   *rels;			/* list of VacuumRelation, or NIL for all */
    -	bool		is_vacuumcmd;	/* true for VACUUM, false for ANALYZE */
    +	bool		is_vacuumcmd;	/* true for VACUUM, false otherwise */
     } VacuumStmt;
    =20
     /*
    @@ -3993,6 +3981,27 @@ typedef struct VacuumRelation
     	List	   *va_cols;		/* list of column names, or NIL for all */
     } VacuumRelation;
    =20
    +/* ----------------------
    + *		Repack Statement
    + * ----------------------
    + */
    +typedef enum RepackCommand
    +{
    +	REPACK_COMMAND_CLUSTER =3D 1,
    +	REPACK_COMMAND_REPACK,
    +	REPACK_COMMAND_VACUUMFULL,
    +} RepackCommand;
    +
    +typedef struct RepackStmt
    +{
    +	NodeTag		type;
    +	RepackCommand command;		/* type of command being run */
    +	VacuumRelation *relation;	/* relation being repacked */
    +	char	   *indexname;		/* order tuples by this index */
    +	bool		usingindex;		/* whether USING INDEX is specified */
    +	List	   *params;			/* list of DefElem nodes */
    +} RepackStmt;
    +
     /* ----------------------
      *		Explain Statement
      *
    diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
    index 5d4fe27ef96..f1a1d5e7a80 100644
    --- a/src/include/parser/kwlist.h
    +++ b/src/include/parser/kwlist.h
    @@ -376,6 +376,7 @@ PG_KEYWORD("reindex", REINDEX, UNRESERVED_KEYWORD, BARE=
    _LABEL)
     PG_KEYWORD("relative", RELATIVE_P, UNRESERVED_KEYWORD, BARE_LABEL)
     PG_KEYWORD("release", RELEASE, UNRESERVED_KEYWORD, BARE_LABEL)
     PG_KEYWORD("rename", RENAME, UNRESERVED_KEYWORD, BARE_LABEL)
    +PG_KEYWORD("repack", REPACK, UNRESERVED_KEYWORD, BARE_LABEL)
     PG_KEYWORD("repeatable", REPEATABLE, UNRESERVED_KEYWORD, BARE_LABEL)
     PG_KEYWORD("replace", REPLACE, UNRESERVED_KEYWORD, BARE_LABEL)
     PG_KEYWORD("replica", REPLICA, UNRESERVED_KEYWORD, BARE_LABEL)
    diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
    index c4606d65043..66690f1134a 100644
    --- a/src/include/tcop/cmdtaglist.h
    +++ b/src/include/tcop/cmdtaglist.h
    @@ -196,6 +196,7 @@ PG_CMDTAG(CMDTAG_REASSIGN_OWNED, "REASSIGN OWNED", fals=
    e, false, false)
     PG_CMDTAG(CMDTAG_REFRESH_MATERIALIZED_VIEW, "REFRESH MATERIALIZED VIEW", t=
    rue, false, false)
     PG_CMDTAG(CMDTAG_REINDEX, "REINDEX", true, false, false)
     PG_CMDTAG(CMDTAG_RELEASE, "RELEASE", false, false, false)
    +PG_CMDTAG(CMDTAG_REPACK, "REPACK", false, false, false)
     PG_CMDTAG(CMDTAG_RESET, "RESET", false, false, false)
     PG_CMDTAG(CMDTAG_REVOKE, "REVOKE", true, false, false)
     PG_CMDTAG(CMDTAG_REVOKE_ROLE, "REVOKE ROLE", false, false, false)
    diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backe=
    nd_progress.h
    index dda813ab407..e69e366dcdc 100644
    --- a/src/include/utils/backend_progress.h
    +++ b/src/include/utils/backend_progress.h
    @@ -28,6 +28,7 @@ typedef enum ProgressCommandType
     	PROGRESS_COMMAND_CREATE_INDEX,
     	PROGRESS_COMMAND_BASEBACKUP,
     	PROGRESS_COMMAND_COPY,
    +	PROGRESS_COMMAND_REPACK,
     } ProgressCommandType;
    =20
     #define PGSTAT_NUM_PROGRESS_PARAM	20
    diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expec=
    ted/cluster.out
    index 4d40a6809ab..277854418fa 100644
    --- a/src/test/regress/expected/cluster.out
    +++ b/src/test/regress/expected/cluster.out
    @@ -495,6 +495,43 @@ ALTER TABLE clstrpart SET WITHOUT CLUSTER;
     ERROR:  cannot mark index clustered in partitioned table
     ALTER TABLE clstrpart CLUSTER ON clstrpart_idx;
     ERROR:  cannot mark index clustered in partitioned table
    +-- Check that REPACK sets new relfilenodes: it should process exactly the =
    same
    +-- tables as CLUSTER did.
    +DROP TABLE old_cluster_info;
    +DROP TABLE new_cluster_info;
    +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, =
    relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class=
     c ON c.oid=3Dtree.relid ;
    +REPACK clstrpart USING INDEX clstrpart_idx;
    +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, =
    relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class=
     c ON c.oid=3Dtree.relid ;
    +SELECT relname, old.level, old.relkind, old.relfilenode =3D new.relfilenod=
    e FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname)=
     ORDER BY relname COLLATE "C";
    +   relname   | level | relkind | ?column?=20
    +-------------+-------+---------+----------
    + clstrpart   |     0 | p       | t
    + clstrpart1  |     1 | p       | t
    + clstrpart11 |     2 | r       | f
    + clstrpart12 |     2 | p       | t
    + clstrpart2  |     1 | r       | f
    + clstrpart3  |     1 | p       | t
    + clstrpart33 |     2 | r       | f
    +(7 rows)
    +
    +-- And finally the same for REPACK w/o index.
    +DROP TABLE old_cluster_info;
    +DROP TABLE new_cluster_info;
    +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, =
    relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class=
     c ON c.oid=3Dtree.relid ;
    +REPACK clstrpart;
    +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, =
    relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class=
     c ON c.oid=3Dtree.relid ;
    +SELECT relname, old.level, old.relkind, old.relfilenode =3D new.relfilenod=
    e FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname)=
     ORDER BY relname COLLATE "C";
    +   relname   | level | relkind | ?column?=20
    +-------------+-------+---------+----------
    + clstrpart   |     0 | p       | t
    + clstrpart1  |     1 | p       | t
    + clstrpart11 |     2 | r       | f
    + clstrpart12 |     2 | p       | t
    + clstrpart2  |     1 | r       | f
    + clstrpart3  |     1 | p       | t
    + clstrpart33 |     2 | r       | f
    +(7 rows)
    +
     DROP TABLE clstrpart;
     -- Ownership of partitions is checked
     CREATE TABLE ptnowner(i int unique) PARTITION BY LIST (i);
    @@ -513,7 +550,7 @@ CREATE TEMP TABLE ptnowner_oldnodes AS
       JOIN pg_class AS c ON c.oid=3Dtree.relid;
     SET SESSION AUTHORIZATION regress_ptnowner;
     CLUSTER ptnowner USING ptnowner_i_idx;
    -WARNING:  permission denied to cluster "ptnowner2", skipping it
    +WARNING:  permission denied to execute CLUSTER on "ptnowner2", skipping it
     RESET SESSION AUTHORIZATION;
     SELECT a.relname, a.relfilenode=3Db.relfilenode FROM pg_class a
       JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C";
    @@ -665,6 +702,101 @@ SELECT * FROM clstr_expression WHERE -a =3D -3 ORDER =
    BY -a, b;
     (4 rows)
    =20
     COMMIT;
    +----------------------------------------------------------------------
    +--
    +-- REPACK
    +--
    +----------------------------------------------------------------------
    +-- REPACK handles individual tables identically to CLUSTER, but it's worth
    +-- checking if it handles table hierarchies identically as well.
    +REPACK clstr_tst USING INDEX clstr_tst_c;
    +-- Verify that inheritance link still works
    +INSERT INTO clstr_tst_inh VALUES (0, 100, 'in child table 2');
    +SELECT a,b,c,substring(d for 30), length(d) from clstr_tst;
    + a  |  b  |        c         |           substring            | length=20
    +----+-----+------------------+--------------------------------+--------
    + 10 |  14 | catorce          |                                |=20=20=20=
    =20=20=20=20
    + 18 |   5 | cinco            |                                |=20=20=20=
    =20=20=20=20
    +  9 |   4 | cuatro           |                                |=20=20=20=
    =20=20=20=20
    + 26 |  19 | diecinueve       |                                |=20=20=20=
    =20=20=20=20
    + 12 |  18 | dieciocho        |                                |=20=20=20=
    =20=20=20=20
    + 30 |  16 | dieciseis        |                                |=20=20=20=
    =20=20=20=20
    + 24 |  17 | diecisiete       |                                |=20=20=20=
    =20=20=20=20
    +  2 |  10 | diez             |                                |=20=20=20=
    =20=20=20=20
    + 23 |  12 | doce             |                                |=20=20=20=
    =20=20=20=20
    + 11 |   2 | dos              |                                |=20=20=20=
    =20=20=20=20
    + 25 |   9 | nueve            |                                |=20=20=20=
    =20=20=20=20
    + 31 |   8 | ocho             |                                |=20=20=20=
    =20=20=20=20
    +  1 |  11 | once             |                                |=20=20=20=
    =20=20=20=20
    + 28 |  15 | quince           |                                |=20=20=20=
    =20=20=20=20
    + 32 |   6 | seis             | xyzzyxyzzyxyzzyxyzzyxyzzyxyzzy | 500000
    + 29 |   7 | siete            |                                |=20=20=20=
    =20=20=20=20
    + 15 |  13 | trece            |                                |=20=20=20=
    =20=20=20=20
    + 22 |  30 | treinta          |                                |=20=20=20=
    =20=20=20=20
    + 17 |  32 | treinta y dos    |                                |=20=20=20=
    =20=20=20=20
    +  3 |  31 | treinta y uno    |                                |=20=20=20=
    =20=20=20=20
    +  5 |   3 | tres             |                                |=20=20=20=
    =20=20=20=20
    + 20 |   1 | uno              |                                |=20=20=20=
    =20=20=20=20
    +  6 |  20 | veinte           |                                |=20=20=20=
    =20=20=20=20
    + 14 |  25 | veinticinco      |                                |=20=20=20=
    =20=20=20=20
    + 21 |  24 | veinticuatro     |                                |=20=20=20=
    =20=20=20=20
    +  4 |  22 | veintidos        |                                |=20=20=20=
    =20=20=20=20
    + 19 |  29 | veintinueve      |                                |=20=20=20=
    =20=20=20=20
    + 16 |  28 | veintiocho       |                                |=20=20=20=
    =20=20=20=20
    + 27 |  26 | veintiseis       |                                |=20=20=20=
    =20=20=20=20
    + 13 |  27 | veintisiete      |                                |=20=20=20=
    =20=20=20=20
    +  7 |  23 | veintitres       |                                |=20=20=20=
    =20=20=20=20
    +  8 |  21 | veintiuno        |                                |=20=20=20=
    =20=20=20=20
    +  0 | 100 | in child table   |                                |=20=20=20=
    =20=20=20=20
    +  0 | 100 | in child table 2 |                                |=20=20=20=
    =20=20=20=20
    +(34 rows)
    +
    +-- Verify that foreign key link still works
    +INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail');
    +ERROR:  insert or update on table "clstr_tst" violates foreign key constra=
    int "clstr_tst_con"
    +DETAIL:  Key (b)=3D(1111) is not present in table "clstr_tst_s".
    +SELECT conname FROM pg_constraint WHERE conrelid =3D 'clstr_tst'::regclass
    +ORDER BY 1;
    +       conname=20=20=20=20=20=20=20=20
    +----------------------
    + clstr_tst_a_not_null
    + clstr_tst_con
    + clstr_tst_pkey
    +(3 rows)
    +
    +-- Verify partial analyze works
    +REPACK (ANALYZE) clstr_tst (a);
    +REPACK (ANALYZE) clstr_tst;
    +REPACK (VERBOSE) clstr_tst (a);
    +ERROR:  ANALYZE option must be specified when a column list is provided
    +-- REPACK w/o argument performs no ordering, so we can only check which ta=
    bles
    +-- have the relfilenode changed.
    +RESET SESSION AUTHORIZATION;
    +CREATE TEMP TABLE relnodes_old AS
    +(SELECT relname, relfilenode
    +FROM pg_class
    +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3'));
    +SET SESSION AUTHORIZATION regress_clstr_user;
    +SET client_min_messages =3D ERROR;  -- order of "skipping" warnings may va=
    ry
    +REPACK;
    +RESET client_min_messages;
    +RESET SESSION AUTHORIZATION;
    +CREATE TEMP TABLE relnodes_new AS
    +(SELECT relname, relfilenode
    +FROM pg_class
    +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3'));
    +-- Do the actual comparison. Unlike CLUSTER, clstr_3 should have been
    +-- processed because there is nothing like clustering index here.
    +SELECT o.relname FROM relnodes_old o
    +JOIN relnodes_new n ON o.relname =3D n.relname
    +WHERE o.relfilenode <> n.relfilenode
    +ORDER BY o.relname;
    + relname=20
    +---------
    + clstr_1
    + clstr_3
    +(2 rows)
    +
     -- clean up
     DROP TABLE clustertest;
     DROP TABLE clstr_1;
    diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expecte=
    d/rules.out
    index 94e45dd4d57..1c957f12d27 100644
    --- a/src/test/regress/expected/rules.out
    +++ b/src/test/regress/expected/rules.out
    @@ -1995,34 +1995,23 @@ pg_stat_progress_basebackup| SELECT pid,
                 ELSE NULL::text
             END AS backup_type
        FROM pg_stat_get_progress_info('BASEBACKUP'::text) s(pid, datid, relid,=
     param1, param2, param3, param4, param5, param6, param7, param8, param9, pa=
    ram10, param11, param12, param13, param14, param15, param16, param17, param=
    18, param19, param20);
    -pg_stat_progress_cluster| SELECT s.pid,
    -    s.datid,
    -    d.datname,
    -    s.relid,
    -        CASE s.param1
    -            WHEN 1 THEN 'CLUSTER'::text
    -            WHEN 2 THEN 'VACUUM FULL'::text
    -            ELSE NULL::text
    +pg_stat_progress_cluster| SELECT pid,
    +    datid,
    +    datname,
    +    relid,
    +        CASE
    +            WHEN (command =3D ANY (ARRAY['CLUSTER'::text, 'VACUUM FULL'::t=
    ext])) THEN command
    +            WHEN (repack_index_relid =3D (0)::oid) THEN 'VACUUM FULL'::text
    +            ELSE 'CLUSTER'::text
             END AS command,
    -        CASE s.param2
    -            WHEN 0 THEN 'initializing'::text
    -            WHEN 1 THEN 'seq scanning heap'::text
    -            WHEN 2 THEN 'index scanning heap'::text
    -            WHEN 3 THEN 'sorting tuples'::text
    -            WHEN 4 THEN 'writing new heap'::text
    -            WHEN 5 THEN 'swapping relation files'::text
    -            WHEN 6 THEN 'rebuilding index'::text
    -            WHEN 7 THEN 'performing final cleanup'::text
    -            ELSE NULL::text
    -        END AS phase,
    -    (s.param3)::oid AS cluster_index_relid,
    -    s.param4 AS heap_tuples_scanned,
    -    s.param5 AS heap_tuples_written,
    -    s.param6 AS heap_blks_total,
    -    s.param7 AS heap_blks_scanned,
    -    s.param8 AS index_rebuild_count
    -   FROM (pg_stat_get_progress_info('CLUSTER'::text) s(pid, datid, relid, p=
    aram1, param2, param3, param4, param5, param6, param7, param8, param9, para=
    m10, param11, param12, param13, param14, param15, param16, param17, param18=
    , param19, param20)
    -     LEFT JOIN pg_database d ON ((s.datid =3D d.oid)));
    +    phase,
    +    repack_index_relid AS cluster_index_relid,
    +    heap_tuples_scanned,
    +    heap_tuples_written,
    +    heap_blks_total,
    +    heap_blks_scanned,
    +    index_rebuild_count
    +   FROM pg_stat_progress_repack;
     pg_stat_progress_copy| SELECT s.pid,
         s.datid,
         d.datname,
    @@ -2082,6 +2071,35 @@ pg_stat_progress_create_index| SELECT s.pid,
         s.param15 AS partitions_done
        FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, rel=
    id, param1, param2, param3, param4, param5, param6, param7, param8, param9,=
     param10, param11, param12, param13, param14, param15, param16, param17, pa=
    ram18, param19, param20)
          LEFT JOIN pg_database d ON ((s.datid =3D d.oid)));
    +pg_stat_progress_repack| SELECT s.pid,
    +    s.datid,
    +    d.datname,
    +    s.relid,
    +        CASE s.param1
    +            WHEN 1 THEN 'CLUSTER'::text
    +            WHEN 2 THEN 'REPACK'::text
    +            WHEN 3 THEN 'VACUUM FULL'::text
    +            ELSE NULL::text
    +        END AS command,
    +        CASE s.param2
    +            WHEN 0 THEN 'initializing'::text
    +            WHEN 1 THEN 'seq scanning heap'::text
    +            WHEN 2 THEN 'index scanning heap'::text
    +            WHEN 3 THEN 'sorting tuples'::text
    +            WHEN 4 THEN 'writing new heap'::text
    +            WHEN 5 THEN 'swapping relation files'::text
    +            WHEN 6 THEN 'rebuilding index'::text
    +            WHEN 7 THEN 'performing final cleanup'::text
    +            ELSE NULL::text
    +        END AS phase,
    +    (s.param3)::oid AS repack_index_relid,
    +    s.param4 AS heap_tuples_scanned,
    +    s.param5 AS heap_tuples_written,
    +    s.param6 AS heap_blks_total,
    +    s.param7 AS heap_blks_scanned,
    +    s.param8 AS index_rebuild_count
    +   FROM (pg_stat_get_progress_info('REPACK'::text) s(pid, datid, relid, pa=
    ram1, param2, param3, param4, param5, param6, param7, param8, param9, param=
    10, param11, param12, param13, param14, param15, param16, param17, param18,=
     param19, param20)
    +     LEFT JOIN pg_database d ON ((s.datid =3D d.oid)));
     pg_stat_progress_vacuum| SELECT s.pid,
         s.datid,
         d.datname,
    diff --git a/src/test/regress/sql/cluster.sql b/src/test/regress/sql/cluste=
    r.sql
    index b7115f86104..c976823a3cb 100644
    --- a/src/test/regress/sql/cluster.sql
    +++ b/src/test/regress/sql/cluster.sql
    @@ -76,7 +76,6 @@ INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should f=
    ail');
     SELECT conname FROM pg_constraint WHERE conrelid =3D 'clstr_tst'::regclass
     ORDER BY 1;
    =20
    -
     SELECT relname, relkind,
         EXISTS(SELECT 1 FROM pg_class WHERE oid =3D c.reltoastrelid) AS hastoa=
    st
     FROM pg_class c WHERE relname LIKE 'clstr_tst%' ORDER BY relname;
    @@ -229,6 +228,24 @@ SELECT relname, old.level, old.relkind, old.relfilenod=
    e =3D new.relfilenode FROM o
     CLUSTER clstrpart;
     ALTER TABLE clstrpart SET WITHOUT CLUSTER;
     ALTER TABLE clstrpart CLUSTER ON clstrpart_idx;
    +
    +-- Check that REPACK sets new relfilenodes: it should process exactly the =
    same
    +-- tables as CLUSTER did.
    +DROP TABLE old_cluster_info;
    +DROP TABLE new_cluster_info;
    +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, =
    relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class=
     c ON c.oid=3Dtree.relid ;
    +REPACK clstrpart USING INDEX clstrpart_idx;
    +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, =
    relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class=
     c ON c.oid=3Dtree.relid ;
    +SELECT relname, old.level, old.relkind, old.relfilenode =3D new.relfilenod=
    e FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname)=
     ORDER BY relname COLLATE "C";
    +
    +-- And finally the same for REPACK w/o index.
    +DROP TABLE old_cluster_info;
    +DROP TABLE new_cluster_info;
    +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, =
    relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class=
     c ON c.oid=3Dtree.relid ;
    +REPACK clstrpart;
    +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, =
    relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class=
     c ON c.oid=3Dtree.relid ;
    +SELECT relname, old.level, old.relkind, old.relfilenode =3D new.relfilenod=
    e FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname)=
     ORDER BY relname COLLATE "C";
    +
     DROP TABLE clstrpart;
    =20
     -- Ownership of partitions is checked
    @@ -313,6 +330,57 @@ EXPLAIN (COSTS OFF) SELECT * FROM clstr_expression WHE=
    RE -a =3D -3 ORDER BY -a, b;
     SELECT * FROM clstr_expression WHERE -a =3D -3 ORDER BY -a, b;
     COMMIT;
    =20
    +----------------------------------------------------------------------
    +--
    +-- REPACK
    +--
    +----------------------------------------------------------------------
    +
    +-- REPACK handles individual tables identically to CLUSTER, but it's worth
    +-- checking if it handles table hierarchies identically as well.
    +REPACK clstr_tst USING INDEX clstr_tst_c;
    +
    +-- Verify that inheritance link still works
    +INSERT INTO clstr_tst_inh VALUES (0, 100, 'in child table 2');
    +SELECT a,b,c,substring(d for 30), length(d) from clstr_tst;
    +
    +-- Verify that foreign key link still works
    +INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail');
    +
    +SELECT conname FROM pg_constraint WHERE conrelid =3D 'clstr_tst'::regclass
    +ORDER BY 1;
    +
    +-- Verify partial analyze works
    +REPACK (ANALYZE) clstr_tst (a);
    +REPACK (ANALYZE) clstr_tst;
    +REPACK (VERBOSE) clstr_tst (a);
    +
    +-- REPACK w/o argument performs no ordering, so we can only check which ta=
    bles
    +-- have the relfilenode changed.
    +RESET SESSION AUTHORIZATION;
    +CREATE TEMP TABLE relnodes_old AS
    +(SELECT relname, relfilenode
    +FROM pg_class
    +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3'));
    +
    +SET SESSION AUTHORIZATION regress_clstr_user;
    +SET client_min_messages =3D ERROR;  -- order of "skipping" warnings may va=
    ry
    +REPACK;
    +RESET client_min_messages;
    +
    +RESET SESSION AUTHORIZATION;
    +CREATE TEMP TABLE relnodes_new AS
    +(SELECT relname, relfilenode
    +FROM pg_class
    +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3'));
    +
    +-- Do the actual comparison. Unlike CLUSTER, clstr_3 should have been
    +-- processed because there is nothing like clustering index here.
    +SELECT o.relname FROM relnodes_old o
    +JOIN relnodes_new n ON o.relname =3D n.relname
    +WHERE o.relfilenode <> n.relfilenode
    +ORDER BY o.relname;
    +
     -- clean up
     DROP TABLE clustertest;
     DROP TABLE clstr_1;
    diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs=
    .list
    index c1ad80a418d..4641da9b746 100644
    --- a/src/tools/pgindent/typedefs.list
    +++ b/src/tools/pgindent/typedefs.list
    @@ -2555,6 +2555,8 @@ ReorderBufferTupleCidKey
     ReorderBufferUpdateProgressTxnCB
     ReorderTuple
     RepOriginId
    +RepackCommand
    +RepackStmt
     ReparameterizeForeignPathByChild_function
     ReplaceVarsFromTargetList_context
     ReplaceVarsNoMatchOption
    --=20
    2.47.3
    
    
    --=-=-=
    Content-Type: text/x-diff
    Content-Disposition: attachment;
     filename=v26-0002-Refactor-index_concurrently_create_copy-for-use-with.patch