Use attnum to identify index columns in pg_restore_attribute_stats().
authorTom Lane <[email protected]>
Wed, 26 Feb 2025 21:36:11 +0000 (16:36 -0500)
committerTom Lane <[email protected]>
Wed, 26 Feb 2025 21:36:20 +0000 (16:36 -0500)
Previously we used attname for both table and index columns, but
that is problematic for indexes because their attnames are assigned
by internal rules that don't guarantee to preserve the names across
dump and reload.  (This is what's causing the remaining buildfarm
failures in cross-version-upgrade tests.)  Fortunately we can use
attnum instead, since there's no such thing as adding or dropping
columns in an existing index.  We met this same problem previously
with ALTER INDEX ... SET STATISTICS, and solved it the same way,
cf commit 5b6d13eec.

In pg_restore_attribute_stats() itself, we accept either attnum or
attname, but the policy used by pg_dump is to always use attname
for tables and attnum for indexes.

Author: Tom Lane <[email protected]>
Author: Corey Huinker <[email protected]>
Discussion: https://postgr.es/m/1457469.1740419458@sss.pgh.pa.us

doc/src/sgml/func.sgml
src/backend/statistics/attribute_stats.c
src/bin/pg_dump/pg_dump.c
src/bin/pg_dump/pg_dump.h
src/bin/pg_dump/t/002_pg_dump.pl
src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm
src/test/regress/expected/stats_import.out
src/test/regress/sql/stats_import.sql

index 12206e0cfc6785886647ebc4ad6fa078c74ed624..0e6c534965254e86561a821c7a3933ddbf6e2fc2 100644 (file)
@@ -30209,8 +30209,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
 </programlisting>
         </para>
         <para>
-         For example, to set the <structname>relpages</structname> and
-         <structname>reltuples</structname> of the table
+         For example, to set the <structfield>relpages</structfield> and
+         <structfield>reltuples</structfield> values for the table
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_relation_stats(
@@ -30222,8 +30222,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <para>
          The argument <literal>relation</literal> with a value of type
          <type>regclass</type> is required, and specifies the table. Other
-         arguments are the names of statistics corresponding to certain
-         columns in <link
+         arguments are the names and values of statistics corresponding to
+         certain columns in <link
          linkend="catalog-pg-class"><structname>pg_class</structname></link>.
          The currently-supported relation statistics are
          <literal>relpages</literal> with a value of type
@@ -30232,16 +30232,16 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          value of type <type>integer</type>.
         </para>
         <para>
-         Additionally, this function supports argument name
+         Additionally, this function accepts argument name
          <literal>version</literal> of type <type>integer</type>, which
-         specifies the version from which the statistics originated, improving
-         interpretation of statistics from older versions of
-         <productname>PostgreSQL</productname>.
+         specifies the server version from which the statistics originated.
+         This is anticipated to be helpful in porting statistics from older
+         versions of <productname>PostgreSQL</productname>.
         </para>
         <para>
          Minor errors are reported as a <literal>WARNING</literal> and
          ignored, and remaining statistics will still be restored. If all
-         specified statistics are successfully restored, return
+         specified statistics are successfully restored, returns
          <literal>true</literal>, otherwise <literal>false</literal>.
         </para>
         <para>
@@ -30281,7 +30281,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <returnvalue>boolean</returnvalue>
        </para>
         <para>
-         Create or update column-level statistics.  Ordinarily, these
+         Creates or updates column-level statistics.  Ordinarily, these
          statistics are collected automatically or updated as a part of <xref
          linkend="sql-vacuum"/> or <xref linkend="sql-analyze"/>, so it's not
          necessary to call this function.  However, it is useful after a
@@ -30300,9 +30300,9 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
 </programlisting>
         </para>
         <para>
-         For example, to set the <structname>avg_width</structname> and
-         <structname>null_frac</structname> for the attribute
-         <structname>col1</structname> of the table
+         For example, to set the <structfield>avg_width</structfield> and
+         <structfield>null_frac</structfield> values for the attribute
+         <structfield>col1</structfield> of the table
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_attribute_stats(
@@ -30315,25 +30315,26 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         </para>
         <para>
          The required arguments are <literal>relation</literal> with a value
-         of type <type>regclass</type>, which specifies the table;
-         <literal>attname</literal> with a value of type <type>name</type>,
+         of type <type>regclass</type>, which specifies the table; either
+         <literal>attname</literal> with a value of type <type>name</type> or
+         <literal>attnum</literal> with a value of type <type>smallint</type>,
          which specifies the column; and <literal>inherited</literal>, which
-         specifies whether the statistics includes values from child tables.
-         Other arguments are the names of statistics corresponding to columns
-         in <link
+         specifies whether the statistics include values from child tables.
+         Other arguments are the names and values of statistics corresponding
+         to columns in <link
          linkend="view-pg-stats"><structname>pg_stats</structname></link>.
         </para>
         <para>
-         Additionally, this function supports argument name
+         Additionally, this function accepts argument name
          <literal>version</literal> of type <type>integer</type>, which
-         specifies the version from which the statistics originated, improving
-         interpretation of statistics from older versions of
-         <productname>PostgreSQL</productname>.
+         specifies the server version from which the statistics originated.
+         This is anticipated to be helpful in porting statistics from older
+         versions of <productname>PostgreSQL</productname>.
         </para>
         <para>
          Minor errors are reported as a <literal>WARNING</literal> and
          ignored, and remaining statistics will still be restored. If all
-         specified statistics are successfully restored, return
+         specified statistics are successfully restored, returns
          <literal>true</literal>, otherwise <literal>false</literal>.
         </para>
         <para>
index 66a5676c810ac8e3f7b64a99f3b08a949eb15af9..6bcbee0edba779cf249407511fdde7d3cac0eb55 100644 (file)
@@ -38,6 +38,7 @@ enum attribute_stats_argnum
 {
    ATTRELATION_ARG = 0,
    ATTNAME_ARG,
+   ATTNUM_ARG,
    INHERITED_ARG,
    NULL_FRAC_ARG,
    AVG_WIDTH_ARG,
@@ -59,6 +60,7 @@ static struct StatsArgInfo attarginfo[] =
 {
    [ATTRELATION_ARG] = {"relation", REGCLASSOID},
    [ATTNAME_ARG] = {"attname", NAMEOID},
+   [ATTNUM_ARG] = {"attnum", INT2OID},
    [INHERITED_ARG] = {"inherited", BOOLOID},
    [NULL_FRAC_ARG] = {"null_frac", FLOAT4OID},
    [AVG_WIDTH_ARG] = {"avg_width", INT4OID},
@@ -76,6 +78,22 @@ static struct StatsArgInfo attarginfo[] =
    [NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
 
+enum clear_attribute_stats_argnum
+{
+   C_ATTRELATION_ARG = 0,
+   C_ATTNAME_ARG,
+   C_INHERITED_ARG,
+   C_NUM_ATTRIBUTE_STATS_ARGS
+};
+
+static struct StatsArgInfo cleararginfo[] =
+{
+   [C_ATTRELATION_ARG] = {"relation", REGCLASSOID},
+   [C_ATTNAME_ARG] = {"attname", NAMEOID},
+   [C_INHERITED_ARG] = {"inherited", BOOLOID},
+   [C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
+};
+
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
 static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
@@ -116,9 +134,9 @@ static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
 {
    Oid         reloid;
-   Name        attname;
-   bool        inherited;
+   char       *attname;
    AttrNumber  attnum;
+   bool        inherited;
 
    Relation    starel;
    HeapTuple   statup;
@@ -164,21 +182,51 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
    /* lock before looking up attribute */
    stats_lock_check_privileges(reloid);
 
-   stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
-   attname = PG_GETARG_NAME(ATTNAME_ARG);
-   attnum = get_attnum(reloid, NameStr(*attname));
+   /* user can specify either attname or attnum, but not both */
+   if (!PG_ARGISNULL(ATTNAME_ARG))
+   {
+       Name        attnamename;
+
+       if (!PG_ARGISNULL(ATTNUM_ARG))
+           ereport(ERROR,
+                   (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                    errmsg("cannot specify both attname and attnum")));
+       attnamename = PG_GETARG_NAME(ATTNAME_ARG);
+       attname = NameStr(*attnamename);
+       attnum = get_attnum(reloid, attname);
+       /* note that this test covers attisdropped cases too: */
+       if (attnum == InvalidAttrNumber)
+           ereport(ERROR,
+                   (errcode(ERRCODE_UNDEFINED_COLUMN),
+                    errmsg("column \"%s\" of relation \"%s\" does not exist",
+                           attname, get_rel_name(reloid))));
+   }
+   else if (!PG_ARGISNULL(ATTNUM_ARG))
+   {
+       attnum = PG_GETARG_INT16(ATTNUM_ARG);
+       attname = get_attname(reloid, attnum, true);
+       /* annoyingly, get_attname doesn't check attisdropped */
+       if (attname == NULL ||
+           !SearchSysCacheExistsAttName(reloid, attname))
+           ereport(ERROR,
+                   (errcode(ERRCODE_UNDEFINED_COLUMN),
+                    errmsg("column %d of relation \"%s\" does not exist",
+                           attnum, get_rel_name(reloid))));
+   }
+   else
+   {
+       ereport(ERROR,
+               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                errmsg("must specify either attname or attnum")));
+       attname = NULL;         /* keep compiler quiet */
+       attnum = 0;
+   }
 
    if (attnum < 0)
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                 errmsg("cannot modify statistics on system column \"%s\"",
-                       NameStr(*attname))));
-
-   if (attnum == InvalidAttrNumber)
-       ereport(ERROR,
-               (errcode(ERRCODE_UNDEFINED_COLUMN),
-                errmsg("column \"%s\" of relation \"%s\" does not exist",
-                       NameStr(*attname), get_rel_name(reloid))));
+                       attname)));
 
    stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
    inherited = PG_GETARG_BOOL(INHERITED_ARG);
@@ -241,7 +289,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
                                &elemtypid, &elem_eq_opr))
        {
            ereport(WARNING,
-                   (errmsg("unable to determine element type of attribute \"%s\"", NameStr(*attname)),
+                   (errmsg("unable to determine element type of attribute \"%s\"", attname),
                     errdetail("Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.")));
            elemtypid = InvalidOid;
            elem_eq_opr = InvalidOid;
@@ -257,7 +305,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
    {
        ereport(WARNING,
                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                errmsg("could not determine less-than operator for attribute \"%s\"", NameStr(*attname)),
+                errmsg("could not determine less-than operator for attribute \"%s\"", attname),
                 errdetail("Cannot set STATISTIC_KIND_HISTOGRAM or STATISTIC_KIND_CORRELATION.")));
 
        do_histogram = false;
@@ -271,7 +319,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
    {
        ereport(WARNING,
                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                errmsg("attribute \"%s\" is not a range type", NameStr(*attname)),
+                errmsg("attribute \"%s\" is not a range type", attname),
                 errdetail("Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.")));
 
        do_bounds_histogram = false;
@@ -857,8 +905,8 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
    AttrNumber  attnum;
    bool        inherited;
 
-   stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
-   reloid = PG_GETARG_OID(ATTRELATION_ARG);
+   stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELATION_ARG);
+   reloid = PG_GETARG_OID(C_ATTRELATION_ARG);
 
    if (RecoveryInProgress())
        ereport(ERROR,
@@ -868,8 +916,8 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
    stats_lock_check_privileges(reloid);
 
-   stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
-   attname = PG_GETARG_NAME(ATTNAME_ARG);
+   stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
+   attname = PG_GETARG_NAME(C_ATTNAME_ARG);
    attnum = get_attnum(reloid, NameStr(*attname));
 
    if (attnum < 0)
@@ -884,13 +932,39 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
                 errmsg("column \"%s\" of relation \"%s\" does not exist",
                        NameStr(*attname), get_rel_name(reloid))));
 
-   stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
-   inherited = PG_GETARG_BOOL(INHERITED_ARG);
+   stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+   inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
    delete_pg_statistic(reloid, attnum, inherited);
    PG_RETURN_VOID();
 }
 
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * Inserts or replaces a row in pg_statistic for the given relation and
+ * attribute name or number. It takes input parameters that correspond to
+ * columns in the view pg_stats.
+ *
+ * Parameters are given in a pseudo named-attribute style: they must be
+ * pairs of parameter names (as text) and values (of appropriate types).
+ * We do that, rather than using regular named-parameter notation, so
+ * that we can add or change parameters without fear of breaking
+ * carelessly-written calls.
+ *
+ * Parameters null_frac, avg_width, and n_distinct all correspond to NOT NULL
+ * columns in pg_statistic. The remaining parameters all belong to a specific
+ * stakind. Some stakinds require multiple parameters, which must be specified
+ * together (or neither specified).
+ *
+ * Parameters are only superficially validated. Omitting a parameter or
+ * passing NULL leaves the statistic unchanged.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
 Datum
 pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 {
index 0de6c959bb0f6067ee7b877944a4f44228421104..7c38c89bf08cfbbfa2c6d5b3d4a4716e6e9d72a0 100644 (file)
@@ -6819,7 +6819,8 @@ getFuncs(Archive *fout)
  */
 static RelStatsInfo *
 getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
-                     float reltuples, int32 relallvisible, char relkind)
+                     float reltuples, int32 relallvisible, char relkind,
+                     char **indAttNames, int nindAttNames)
 {
    if (!fout->dopt->dumpStatistics)
        return NULL;
@@ -6848,6 +6849,8 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
        info->reltuples = reltuples;
        info->relallvisible = relallvisible;
        info->relkind = relkind;
+       info->indAttNames = indAttNames;
+       info->nindAttNames = nindAttNames;
        info->postponed_def = false;
 
        return info;
@@ -7249,7 +7252,8 @@ getTables(Archive *fout, int *numTables)
        /* Add statistics */
        if (tblinfo[i].interesting)
            getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relpages,
-                                 reltuples, relallvisible, tblinfo[i].relkind);
+                                 reltuples, relallvisible, tblinfo[i].relkind,
+                                 NULL, 0);
 
        /*
         * Read-lock target tables to make sure they aren't DROPPED or altered
@@ -7534,6 +7538,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
                i_contableoid,
                i_conoid,
                i_condef,
+               i_indattnames,
                i_tablespace,
                i_indreloptions,
                i_indstatcols,
@@ -7579,6 +7584,11 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
                         "c.tableoid AS contableoid, "
                         "c.oid AS conoid, "
                         "pg_catalog.pg_get_constraintdef(c.oid, false) AS condef, "
+                        "CASE WHEN i.indexprs IS NOT NULL THEN "
+                        "(SELECT pg_catalog.array_agg(attname ORDER BY attnum)"
+                        "  FROM pg_catalog.pg_attribute "
+                        "  WHERE attrelid = i.indexrelid) "
+                        "ELSE NULL END AS indattnames, "
                         "(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, "
                         "t.reloptions AS indreloptions, ");
 
@@ -7698,6 +7708,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
    i_contableoid = PQfnumber(res, "contableoid");
    i_conoid = PQfnumber(res, "conoid");
    i_condef = PQfnumber(res, "condef");
+   i_indattnames = PQfnumber(res, "indattnames");
    i_tablespace = PQfnumber(res, "tablespace");
    i_indreloptions = PQfnumber(res, "indreloptions");
    i_indstatcols = PQfnumber(res, "indstatcols");
@@ -7714,6 +7725,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
    {
        Oid         indrelid = atooid(PQgetvalue(res, j, i_indrelid));
        TableInfo  *tbinfo = NULL;
+       char      **indAttNames = NULL;
+       int         nindAttNames = 0;
        int         numinds;
 
        /* Count rows for this table */
@@ -7784,10 +7797,18 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
            else
                indexkind = RELKIND_PARTITIONED_INDEX;
 
-           contype = *(PQgetvalue(res, j, i_contype));
+           if (!PQgetisnull(res, j, i_indattnames))
+           {
+               if (!parsePGArray(PQgetvalue(res, j, i_indattnames),
+                                 &indAttNames, &nindAttNames))
+                   pg_fatal("could not parse %s array", "indattnames");
+           }
+
            relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
-                                            reltuples, relallvisible, indexkind);
+                                            reltuples, relallvisible, indexkind,
+                                            indAttNames, nindAttNames);
 
+           contype = *(PQgetvalue(res, j, i_contype));
            if (contype == 'p' || contype == 'u' || contype == 'x')
            {
                /*
@@ -10410,28 +10431,6 @@ dumpComment(Archive *fout, const char *type,
                        catalogId, subid, dumpId, NULL);
 }
 
-/*
- * Tabular description of the parameters to pg_restore_attribute_stats()
- * param_name, param_type
- */
-static const char *att_stats_arginfo[][2] = {
-   {"attname", "name"},
-   {"inherited", "boolean"},
-   {"null_frac", "float4"},
-   {"avg_width", "integer"},
-   {"n_distinct", "float4"},
-   {"most_common_vals", "text"},
-   {"most_common_freqs", "float4[]"},
-   {"histogram_bounds", "text"},
-   {"correlation", "float4"},
-   {"most_common_elems", "text"},
-   {"most_common_elem_freqs", "float4[]"},
-   {"elem_count_histogram", "float4[]"},
-   {"range_length_histogram", "text"},
-   {"range_empty_frac", "float4"},
-   {"range_bounds_histogram", "text"},
-};
-
 /*
  * appendNamedArgument --
  *
@@ -10440,9 +10439,9 @@ static const char *att_stats_arginfo[][2] = {
  */
 static void
 appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
-                   const char *argval, const char *argtype)
+                   const char *argtype, const char *argval)
 {
-   appendPQExpBufferStr(out, "\t");
+   appendPQExpBufferStr(out, ",\n\t");
 
    appendStringLiteralAH(out, argname, fout);
    appendPQExpBufferStr(out, ", ");
@@ -10451,68 +10450,6 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
    appendPQExpBuffer(out, "::%s", argtype);
 }
 
-/*
- * appendRelStatsImport --
- *
- * Append a formatted pg_restore_relation_stats statement.
- */
-static void
-appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo,
-                    const char *qualified_name)
-{
-   char        reltuples_str[FLOAT_SHORTEST_DECIMAL_LEN];
-
-   float_to_shortest_decimal_buf(rsinfo->reltuples, reltuples_str);
-
-   appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-   appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
-                     fout->remoteVersion);
-   appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n", qualified_name);
-   appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-   appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
-   appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
-                     rsinfo->relallvisible);
-}
-
-/*
- * appendAttStatsImport --
- *
- * Append a series of formatted pg_restore_attribute_stats statements.
- */
-static void
-appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res,
-                    const char *qualified_name)
-{
-   for (int rownum = 0; rownum < PQntuples(res); rownum++)
-   {
-       const char *sep = "";
-
-       appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-       appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
-                         fout->remoteVersion);
-       appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n",
-                         qualified_name);
-       for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
-       {
-           const char *argname = att_stats_arginfo[argno][0];
-           const char *argtype = att_stats_arginfo[argno][1];
-           int         fieldno = PQfnumber(res, argname);
-
-           if (fieldno < 0)
-               pg_fatal("attribute stats export query missing field '%s'",
-                        argname);
-
-           if (PQgetisnull(res, rownum, fieldno))
-               continue;
-
-           appendPQExpBufferStr(out, sep);
-           appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
-           sep = ",\n";
-       }
-       appendPQExpBufferStr(out, "\n);\n");
-   }
-}
-
 /*
  * Decide which section to use based on the relkind of the parent object.
  *
@@ -10549,14 +10486,30 @@ statisticsDumpSection(const RelStatsInfo *rsinfo)
 static void
 dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 {
+   const DumpableObject *dobj = &rsinfo->dobj;
    PGresult   *res;
    PQExpBuffer query;
    PQExpBuffer out;
    PQExpBuffer tag;
-   DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
    DumpId     *deps = NULL;
    int         ndeps = 0;
-   const char *qualified_name;
+   char       *qualified_name;
+   char        reltuples_str[FLOAT_SHORTEST_DECIMAL_LEN];
+   int         i_attname;
+   int         i_inherited;
+   int         i_null_frac;
+   int         i_avg_width;
+   int         i_n_distinct;
+   int         i_most_common_vals;
+   int         i_most_common_freqs;
+   int         i_histogram_bounds;
+   int         i_correlation;
+   int         i_most_common_elems;
+   int         i_most_common_elem_freqs;
+   int         i_elem_count_histogram;
+   int         i_range_length_histogram;
+   int         i_range_empty_frac;
+   int         i_range_bounds_histogram;
 
    /* nothing to do if we are not dumping statistics */
    if (!fout->dopt->dumpStatistics)
@@ -10586,7 +10539,8 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
        if (fout->remoteVersion >= 170000)
            appendPQExpBufferStr(query,
-                                "s.range_length_histogram, s.range_empty_frac, "
+                                "s.range_length_histogram, "
+                                "s.range_empty_frac, "
                                 "s.range_bounds_histogram ");
        else
            appendPQExpBufferStr(query,
@@ -10595,7 +10549,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
                                 "NULL AS range_bounds_histogram ");
 
        appendPQExpBufferStr(query,
-                            "FROM pg_stats s "
+                            "FROM pg_catalog.pg_stats s "
                             "WHERE s.schemaname = $1 "
                             "AND s.tablename = $2 "
                             "ORDER BY s.attname, s.inherited");
@@ -10606,21 +10560,137 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
        resetPQExpBuffer(query);
    }
 
+   out = createPQExpBuffer();
+
+   qualified_name = pg_strdup(fmtQualifiedDumpable(rsinfo));
+
+   /* restore relation stats */
+   appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+   appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+                     fout->remoteVersion);
+   appendPQExpBufferStr(out, "\t'relation', ");
+   appendStringLiteralAH(out, qualified_name, fout);
+   appendPQExpBufferStr(out, "::regclass,\n");
+   appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
+   float_to_shortest_decimal_buf(rsinfo->reltuples, reltuples_str);
+   appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
+   appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
+                     rsinfo->relallvisible);
+
+   /* fetch attribute stats */
    appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
    appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
    appendPQExpBufferStr(query, ", ");
    appendStringLiteralAH(query, dobj->name, fout);
-   appendPQExpBufferStr(query, "); ");
+   appendPQExpBufferStr(query, ");");
 
    res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
-   out = createPQExpBuffer();
+   i_attname = PQfnumber(res, "attname");
+   i_inherited = PQfnumber(res, "inherited");
+   i_null_frac = PQfnumber(res, "null_frac");
+   i_avg_width = PQfnumber(res, "avg_width");
+   i_n_distinct = PQfnumber(res, "n_distinct");
+   i_most_common_vals = PQfnumber(res, "most_common_vals");
+   i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+   i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+   i_correlation = PQfnumber(res, "correlation");
+   i_most_common_elems = PQfnumber(res, "most_common_elems");
+   i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+   i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+   i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+   i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+   i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+
+   /* restore attribute stats */
+   for (int rownum = 0; rownum < PQntuples(res); rownum++)
+   {
+       const char *attname;
 
-   qualified_name = fmtQualifiedId(rsinfo->dobj.namespace->dobj.name,
-                                   rsinfo->dobj.name);
+       appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+       appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+                         fout->remoteVersion);
+       appendPQExpBufferStr(out, "\t'relation', ");
+       appendStringLiteralAH(out, qualified_name, fout);
+       appendPQExpBufferStr(out, "::regclass");
+
+       if (PQgetisnull(res, rownum, i_attname))
+           pg_fatal("attname cannot be NULL");
+       attname = PQgetvalue(res, rownum, i_attname);
+
+       /*
+        * Indexes look up attname in indAttNames to derive attnum, all others
+        * use attname directly.  We must specify attnum for indexes, since
+        * their attnames are not necessarily stable across dump/reload.
+        */
+       if (rsinfo->nindAttNames == 0)
+           appendNamedArgument(out, fout, "attname", "name", attname);
+       else
+       {
+           bool        found = false;
+
+           for (int i = 0; i < rsinfo->nindAttNames; i++)
+           {
+               if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
+               {
+                   appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+                                     i + 1);
+                   found = true;
+                   break;
+               }
+           }
 
-   appendRelStatsImport(out, fout, rsinfo, qualified_name);
-   appendAttStatsImport(out, fout, res, qualified_name);
+           if (!found)
+               pg_fatal("could not find index attname \"%s\"", attname);
+       }
+
+       if (!PQgetisnull(res, rownum, i_inherited))
+           appendNamedArgument(out, fout, "inherited", "boolean",
+                               PQgetvalue(res, rownum, i_inherited));
+       if (!PQgetisnull(res, rownum, i_null_frac))
+           appendNamedArgument(out, fout, "null_frac", "real",
+                               PQgetvalue(res, rownum, i_null_frac));
+       if (!PQgetisnull(res, rownum, i_avg_width))
+           appendNamedArgument(out, fout, "avg_width", "integer",
+                               PQgetvalue(res, rownum, i_avg_width));
+       if (!PQgetisnull(res, rownum, i_n_distinct))
+           appendNamedArgument(out, fout, "n_distinct", "real",
+                               PQgetvalue(res, rownum, i_n_distinct));
+       if (!PQgetisnull(res, rownum, i_most_common_vals))
+           appendNamedArgument(out, fout, "most_common_vals", "text",
+                               PQgetvalue(res, rownum, i_most_common_vals));
+       if (!PQgetisnull(res, rownum, i_most_common_freqs))
+           appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+                               PQgetvalue(res, rownum, i_most_common_freqs));
+       if (!PQgetisnull(res, rownum, i_histogram_bounds))
+           appendNamedArgument(out, fout, "histogram_bounds", "text",
+                               PQgetvalue(res, rownum, i_histogram_bounds));
+       if (!PQgetisnull(res, rownum, i_correlation))
+           appendNamedArgument(out, fout, "correlation", "real",
+                               PQgetvalue(res, rownum, i_correlation));
+       if (!PQgetisnull(res, rownum, i_most_common_elems))
+           appendNamedArgument(out, fout, "most_common_elems", "text",
+                               PQgetvalue(res, rownum, i_most_common_elems));
+       if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
+           appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+                               PQgetvalue(res, rownum, i_most_common_elem_freqs));
+       if (!PQgetisnull(res, rownum, i_elem_count_histogram))
+           appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+                               PQgetvalue(res, rownum, i_elem_count_histogram));
+       if (fout->remoteVersion >= 170000)
+       {
+           if (!PQgetisnull(res, rownum, i_range_length_histogram))
+               appendNamedArgument(out, fout, "range_length_histogram", "text",
+                                   PQgetvalue(res, rownum, i_range_length_histogram));
+           if (!PQgetisnull(res, rownum, i_range_empty_frac))
+               appendNamedArgument(out, fout, "range_empty_frac", "real",
+                                   PQgetvalue(res, rownum, i_range_empty_frac));
+           if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
+               appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+                                   PQgetvalue(res, rownum, i_range_bounds_histogram));
+       }
+       appendPQExpBufferStr(out, "\n);\n");
+   }
 
    PQclear(res);
 
@@ -10634,8 +10704,9 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
                              .deps = deps,
                              .nDeps = ndeps));
 
-   destroyPQExpBuffer(query);
+   free(qualified_name);
    destroyPQExpBuffer(out);
+   destroyPQExpBuffer(query);
    destroyPQExpBuffer(tag);
 }
 
index 9d6a4857c4bf7d2da87c1d16fb3c414a5277d182..ca32f167878eb8d0e5cb73747b63108e6b917110 100644 (file)
@@ -442,6 +442,13 @@ typedef struct _relStatsInfo
    float       reltuples;
    int32       relallvisible;
    char        relkind;        /* 'r', 'm', 'i', etc */
+
+   /*
+    * indAttNames/nindAttNames are populated only if the relation is an index
+    * with at least one expression column; we don't need them otherwise.
+    */
+   char      **indAttNames;    /* attnames of the index, in order */
+   int32       nindAttNames;   /* number of attnames stored (can be 0) */
    bool        postponed_def;  /* stats must be postponed into post-data */
 } RelStatsInfo;
 
index 3945e4f0e2aa89e6b5cad1b9115e2b5a1bc7f270..c7bffc1b045319a6585829e20e2f9ea57ffe1335 100644 (file)
@@ -4720,24 +4720,41 @@ my %tests = (
            CREATE TABLE dump_test.has_stats
            AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
            CREATE MATERIALIZED VIEW dump_test.has_stats_mv AS SELECT * FROM dump_test.has_stats;
-           CREATE INDEX dup_test_post_data_ix ON dump_test.has_stats((x - 1));
+           CREATE INDEX dup_test_post_data_ix ON dump_test.has_stats(x, (x - 1));
            ANALYZE dump_test.has_stats, dump_test.has_stats_mv;',
-       regexp => qr/pg_catalog.pg_restore_attribute_stats/,
+       regexp => qr/^
+           \QSELECT * FROM pg_catalog.pg_restore_relation_stats(\E\s+
+           'version',\s'\d+'::integer,\s+
+           'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+           'relpages',\s'\d+'::integer,\s+
+           'reltuples',\s'\d+'::real,\s+
+           'relallvisible',\s'\d+'::integer\s+
+           \);\s+
+           \QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
+           'version',\s'\d+'::integer,\s+
+           'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+           'attnum',\s'2'::smallint,\s+
+           'inherited',\s'f'::boolean,\s+
+           'null_frac',\s'0'::real,\s+
+           'avg_width',\s'4'::integer,\s+
+           'n_distinct',\s'-1'::real,\s+
+           'histogram_bounds',\s'\{[0-9,]+\}'::text,\s+
+           'correlation',\s'1'::real\s+
+           \);/xm,
        like => {
            %full_runs,
            %dump_test_schema_runs,
            no_data_no_schema => 1,
            no_schema => 1,
-           section_data => 1,
            section_post_data => 1,
            statistics_only => 1,
-           },
+       },
        unlike => {
            exclude_dump_test_schema => 1,
            no_statistics => 1,
            only_dump_measurement => 1,
            schema_only => 1,
-           },
+       },
    },
 
    #
@@ -4759,11 +4776,11 @@ my %tests = (
            section_data => 1,
            section_post_data => 1,
            statistics_only => 1,
-           },
+       },
        unlike => {
            no_statistics => 1,
            schema_only => 1,
-           },
+       },
    },
 
    # CREATE TABLE with partitioned table and various AMs.  One
index 0a707c69c5c5b5bad14fcfb409561a47af4e2ea4..ec874852d129eda829561b0fb854989fff962ca1 100644 (file)
@@ -345,7 +345,7 @@ sub adjust_old_dumpfile
    {
        $dump =~ s/
            (^SELECT\s\*\sFROM\spg_catalog\.pg_restore_relation_stats\(
-           \s+'relation',\s'public\.hash_[a-z0-9]*_heap'::regclass,
+           [^;]*'relation',\s'public\.hash_[a-z0-9]*_heap'::regclass,
            [^;]*'relallvisible',)\s'\d+'::integer
            /$1 ''::integer/mgx;
    }
@@ -692,7 +692,7 @@ sub adjust_new_dumpfile
    {
        $dump =~ s/
            (^SELECT\s\*\sFROM\spg_catalog\.pg_restore_relation_stats\(
-           \s+'relation',\s'public\.hash_[a-z0-9]*_heap'::regclass,
+           [^;]*'relation',\s'public\.hash_[a-z0-9]*_heap'::regclass,
            [^;]*'relallvisible',)\s'\d+'::integer
            /$1 ''::integer/mgx;
    }
index 7e8b7f429c94e82c189de5ec892e5556111cc6be..1f150f7b08d956d1380091e7650c2f5232d0b2c2 100644 (file)
@@ -278,6 +278,31 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
+    'null_frac', 0.2::real,
+    'avg_width', 5::integer,
+    'n_distinct', 0.6::real);
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.2 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: restore by attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'version', 150000::integer,
     'null_frac', 0.4::real,
     'avg_width', 5::integer,
     'n_distinct', 0.6::real);
@@ -1241,7 +1266,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 ERROR:  "relation" cannot be NULL
--- error: attname null
+-- error: missing attname
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', NULL::name,
@@ -1250,7 +1275,18 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  "attname" cannot be NULL
+ERROR:  must specify either attname or attnum
+-- error: both attname and attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  cannot specify both attname and attnum
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
index 57422750b90758d575b11c48f0924c29aba2bcd8..8c183bceb8aa0742e818f1873724b571de485374 100644 (file)
@@ -184,6 +184,23 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
+    'null_frac', 0.2::real,
+    'avg_width', 5::integer,
+    'n_distinct', 0.6::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: restore by attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'version', 150000::integer,
     'null_frac', 0.4::real,
     'avg_width', 5::integer,
     'n_distinct', 0.6::real);
@@ -902,7 +919,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 
--- error: attname null
+-- error: missing attname
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', NULL::name,
@@ -912,6 +929,17 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 
+-- error: both attname and attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,