From 678e067bc7c4d3cbf8e3f2f4300d3e8d526a34ad Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Sun, 18 Oct 2026 20:55:27 -0400 Subject: [PATCH v1] Handle failed typanalyze when building extended statistics make_build_data() calls examine_expression() a second time, now with the computed statistics target, and did not check for a NULL result. A typanalyze function that rejects the expression only in that call made ANALYZE crash. Skip the statistics object with the usual warning instead. Discussion: https://postgr.es/m/19763-14da3f96c02f07e8@postgresql.org --- src/backend/statistics/extended_stats.c | 24 ++++++++++++++ .../expected/test_custom_types.out | 32 +++++++++++++++++++ .../sql/test_custom_types.sql | 22 +++++++++++++ .../test_custom_types--1.0.sql | 8 +++++ .../test_custom_types/test_custom_types.c | 20 ++++++++++++ 5 files changed, 106 insertions(+) diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c index 515095faa4b..5c2f12efadc 100644 --- a/src/backend/statistics/extended_stats.c +++ b/src/backend/statistics/extended_stats.c @@ -196,6 +196,18 @@ BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows, /* evaluate expressions (if the statistics object has any) */ data = make_build_data(onerel, stat, numrows, rows, stats, stattarget); + if (!data) + { + if (!AmAutoVacuumWorkerProcess()) + ereport(WARNING, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("statistics object \"%s.%s\" could not be computed for relation \"%s.%s\"", + stat->schema, stat->name, + get_namespace_name(onerel->rd_rel->relnamespace), + RelationGetRelationName(onerel)), + errtable(onerel))); + continue; + } /* compute statistic of each requested type */ foreach(lc2, stat->types) @@ -2496,6 +2508,8 @@ statext_expressions_load(Oid stxoid, bool inh, int idx) * Evaluate the expressions, so that we can use the results to build * all the requested statistics types. This matters especially for * expensive expressions, of course. + * + * Returns NULL if one of the expressions cannot be analyzed. */ static StatsBuildData * make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows, @@ -2585,6 +2599,16 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows, result->attnums[idx] = k; result->stats[idx] = examine_expression(expr, stattarget); + /* + * The typanalyze function sees a different attstattarget here than in + * lookup_var_attr_stats(), so it may reject the expression now. + */ + if (result->stats[idx] == NULL) + { + pfree(result); + return NULL; + } + idx++; k--; } diff --git a/src/test/modules/test_custom_types/expected/test_custom_types.out b/src/test/modules/test_custom_types/expected/test_custom_types.out index 59f0917c2d7..b6545df0cf6 100644 --- a/src/test/modules/test_custom_types/expected/test_custom_types.out +++ b/src/test/modules/test_custom_types/expected/test_custom_types.out @@ -173,6 +173,38 @@ SELECT COUNT(*) FROM test_table GROUP BY (func_int_custom(data)); 1 (3 rows) +DROP STATISTICS test_stats; +-- Switch type to use typanalyze function that depends on attstattarget. +ALTER TYPE int_custom SET (ANALYZE = int_custom_typanalyze_stattarget); +-- Extended statistics with an expression accepted by the first lookup +-- but rejected when the statistics are built. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +WARNING: statistics object "public.test_stats" could not be computed for relation "public.test_table" +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; + stxname | expr_stats_is_null +------------+-------------------- + test_stats | t +(1 row) + +DROP STATISTICS test_stats; +-- Same with a single expression, building only expression statistics. +CREATE STATISTICS test_stats ON (func_int_custom(data)) FROM test_table; +ANALYZE test_table; +WARNING: statistics object "public.test_stats" could not be computed for relation "public.test_table" +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; + stxname | expr_stats_is_null +------------+-------------------- + test_stats | t +(1 row) + DROP STATISTICS test_stats; -- Cleanup DROP FUNCTION func_int_custom; diff --git a/src/test/modules/test_custom_types/sql/test_custom_types.sql b/src/test/modules/test_custom_types/sql/test_custom_types.sql index f1c75e4ba5b..afac84dacef 100644 --- a/src/test/modules/test_custom_types/sql/test_custom_types.sql +++ b/src/test/modules/test_custom_types/sql/test_custom_types.sql @@ -101,6 +101,28 @@ SELECT tablename, SELECT COUNT(*) FROM test_table GROUP BY (func_int_custom(data)); DROP STATISTICS test_stats; +-- Switch type to use typanalyze function that depends on attstattarget. +ALTER TYPE int_custom SET (ANALYZE = int_custom_typanalyze_stattarget); + +-- Extended statistics with an expression accepted by the first lookup +-- but rejected when the statistics are built. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; +DROP STATISTICS test_stats; +-- Same with a single expression, building only expression statistics. +CREATE STATISTICS test_stats ON (func_int_custom(data)) FROM test_table; +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; +DROP STATISTICS test_stats; + -- Cleanup DROP FUNCTION func_int_custom; DROP TABLE test_table; diff --git a/src/test/modules/test_custom_types/test_custom_types--1.0.sql b/src/test/modules/test_custom_types/test_custom_types--1.0.sql index ce0e905d636..aa680880e3a 100644 --- a/src/test/modules/test_custom_types/test_custom_types--1.0.sql +++ b/src/test/modules/test_custom_types/test_custom_types--1.0.sql @@ -32,6 +32,14 @@ RETURNS boolean AS 'MODULE_PATHNAME' LANGUAGE C STRICT; +-- +-- Typanalyze function whose result depends on attstattarget +-- +CREATE FUNCTION int_custom_typanalyze_stattarget(internal) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + -- -- The int_custom type definition -- diff --git a/src/test/modules/test_custom_types/test_custom_types.c b/src/test/modules/test_custom_types/test_custom_types.c index 41262044b47..4b5fd504776 100644 --- a/src/test/modules/test_custom_types/test_custom_types.c +++ b/src/test/modules/test_custom_types/test_custom_types.c @@ -28,6 +28,7 @@ PG_FUNCTION_INFO_V1(int_custom_in); PG_FUNCTION_INFO_V1(int_custom_out); PG_FUNCTION_INFO_V1(int_custom_typanalyze_false); PG_FUNCTION_INFO_V1(int_custom_typanalyze_invalid); +PG_FUNCTION_INFO_V1(int_custom_typanalyze_stattarget); PG_FUNCTION_INFO_V1(int_custom_eq); PG_FUNCTION_INFO_V1(int_custom_ne); PG_FUNCTION_INFO_V1(int_custom_lt); @@ -110,6 +111,25 @@ int_custom_typanalyze_invalid(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } +/* + * int_custom_typanalyze_stattarget + * + * This function accepts the column only when attstattarget is negative, + * and returns false otherwise. For expressions in extended statistics, + * the first lookup passes -1 while the build passes the computed target, + * so the two calls give different answers. + */ +Datum +int_custom_typanalyze_stattarget(PG_FUNCTION_ARGS) +{ + VacAttrStats *stats = (VacAttrStats *) PG_GETARG_POINTER(0); + + if (stats->attstattarget >= 0) + PG_RETURN_BOOL(false); + + PG_RETURN_BOOL(std_typanalyze(stats)); +} + /* * Comparison functions for int_custom type */ -- 2.37.1 (Apple Git-137.1)