Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vvm7P-001RzD-0b for pgsql-hackers@arkaria.postgresql.org; Fri, 27 Feb 2026 00:54:11 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1vvm7N-00Gsfp-2l for pgsql-hackers@arkaria.postgresql.org; Fri, 27 Feb 2026 00:54:09 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vvm7N-00GsfX-0E for pgsql-hackers@lists.postgresql.org; Fri, 27 Feb 2026 00:54:09 +0000 Received: from fout-b2-smtp.messagingengine.com ([202.12.124.145]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1vvm7I-00000001OUU-20tT for pgsql-hackers@lists.postgresql.org; Fri, 27 Feb 2026 00:54:08 +0000 Received: from phl-compute-08.internal (phl-compute-08.internal [10.202.2.48]) by mailfout.stl.internal (Postfix) with ESMTP id 83C1E1D00035; Thu, 26 Feb 2026 19:54:04 -0500 (EST) Received: from phl-frontend-04 ([10.202.2.163]) by phl-compute-08.internal (MEProxy); Thu, 26 Feb 2026 19:54:04 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=paquier.xyz; h= cc:cc:content-type:content-type:date:date:from:from:in-reply-to :message-id:mime-version:reply-to:subject:subject:to:to; s=fm3; t=1772153644; x=1772240044; bh=RmAFoLY5QB6wERGDatcZ3s5F1GKFZPm8 26Ly8u+kiO4=; b=pxVAjvvACpziKWIl9iNK+loeolldW7sV/ChMo/DpG5yZ0Eyx TOWrUbCojtGO3S7LwQIcZRc9YzHla/YXvW+lWuA0ruo5cFpCvCeFfuxH5LY73yJ1 ulqSc0dCI9ldEUFSEl1ER/ngdcVsi+6wcUUuJcVfjkBWb0uYAzlWPMjY9n41QQ5N T5WqjW4jDO/EQWrWNotmtCJgb1GmDiPy1l7aKvjCi7Jim+Hx0Q9cscJ53q+oyR93 PnOQ/pie9jbhgURONJg7WxfqbpLgo+ouacH4GsVWVwjSs41o4uhVcfjMbbBPW4EV W/ymDbppAmjFdEVUjiAri6k/UNNzoluyQbj9+A== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:message-id :mime-version:reply-to:subject:subject:to:to:x-me-proxy :x-me-sender:x-me-sender:x-sasl-enc; s=fm3; t=1772153644; x= 1772240044; bh=RmAFoLY5QB6wERGDatcZ3s5F1GKFZPm826Ly8u+kiO4=; b=n kfe2jgphTrLq8D87yy/lhM7CxTLUIKVZOezOOFyzscggRdtiCzIWyZeF1cujR6W+ osXfGJstuWfnTb/YzvuTA5UV5PoKVJdtpS/REdZfjy7xf1iop59+IR64LdDRGqxU 0oXcdY31gXEPFZ80arYtEhlDoGMFjS8zFQQ1AMPAKSfeE0XngcqmwradJxUIFatN uz4kcKiWdSDDSotEKUymoB1IYg5jyES7tgnW9KC7irQ5qKTuy9qThpK18SQICu6c MUL9zNBrFazjHDsNTdQuV9l3D5r8FifdOtVbaflBwKlzJdIKCw8xII0mhshvO8j2 MmWFdu8OsVTeI3r2Rs1wA== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeefgedrtddtgddvgeejheekucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmnegfrh hlucfvnfffucdljedtmdenucfjughrpeffhffvvefukfggtggusehgtderredttddvnecu hfhrohhmpefoihgthhgrvghlucfrrghquhhivghruceomhhitghhrggvlhesphgrqhhuih gvrhdrgiihiieqnecuggftrfgrthhtvghrnhepgeffkedufeeutdffteevgeduvdffvedu uefgudffgfelhfeludeghfejfeffteeinecuvehluhhsthgvrhfuihiivgeptdenucfrrg hrrghmpehmrghilhhfrhhomhepmhhitghhrggvlhesphgrqhhuihgvrhdrgiihiidpnhgs pghrtghpthhtohepfedpmhhouggvpehsmhhtphhouhhtpdhrtghpthhtohepphhgshhqlh dqhhgrtghkvghrsheslhhishhtshdrphhoshhtghhrvghsqhhlrdhorhhgpdhrtghpthht ohepthhomhgrshesvhhonhgurhgrrdhmvgdprhgtphhtthhopegtohhrvgihrdhhuhhinh hkvghrsehgmhgrihhlrdgtohhm X-ME-Proxy: Feedback-ID: i0fe9450f:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA; Thu, 26 Feb 2026 19:54:02 -0500 (EST) Date: Fri, 27 Feb 2026 09:53:58 +0900 From: Michael Paquier To: Postgres hackers Cc: Tomas Vondra , Corey Huinker Subject: Defects with invalid stats data for expressions in extended stats Message-ID: MIME-Version: 1.0 Content-Type: multipart/signed; micalg=pgp-sha512; protocol="application/pgp-signature"; boundary="417ghOodyzBYRSAC" Content-Disposition: inline List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --417ghOodyzBYRSAC Content-Type: multipart/mixed; boundary="0EXhyj2swSESoM8i" Content-Disposition: inline --0EXhyj2swSESoM8i Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Content-Transfer-Encoding: quoted-printable Hi all, (Adding Tomas in CC., as primary author of the feature dealt with here, and Corey, due to his work for the restore of extstats.) While looking at the proposed patch for the restore of extended statistics on expressions, I have bumped into two defects that exist since this feature has been introduced in v15. First, I have thought that this was a problem only related to the proposed patch, but after more analysis, I have found that this issue is independent, and can be triggered without restoring any stats. First, one thing that is important to know is that when defining an extstat object with N expressions, what we finish by storing in the catalogs is an array of N pg_statistics tuples. Some expressions can have invalid data, symbolized by this code in extended_stats.c=20 if (!stats->stats_valid) { astate =3D accumArrayResult(astate, (Datum) 0, true, typOid, CurrentMemoryContext); continue; } There is nothing wrong with that. Having N elements in the array of pg_statistics tuples is a requirement, and the code clearly intends that. There should be no more and no less elements, and this is used as a marker to let the code that loads this catalog data that nothing could be computed. This data is inserted when we run ANALYZE. Some code paths are unfortunately not water-proof with this NULL-ness handling, and I have found two of them as fixed by 0001. 1) When building statistics, lookup_var_attr_stats() has missed the fact that computing stats for an expression could lead to invalid stats being generated. examine_attribute() deals with this case by returning NULL if either: 1-1) the typanalyze callback returns false, 1-2) The number of rows returned is negative. 1-3) For whatever reason in a custom type implementation, the compute_stats callback is not set. lookup_var_attr_stats() has been dealing with the case of invalid stats for attributes, but it has missed the mark after calling examine_attribute() for each expression. Note that examine_attribute() exists in both extended_stats.c and analyze.c, they are very close in shape, and need to rely on the same assumptions in terms of what the typanalyze callback can return. lookup_var_attr_stats() has two callers, both are able to deal with NULL data (aka invalid stats). A consequence of this issue is a set of NULL pointer dereferences for MCV, ndistinct and dependencies, as all these code paths expect something to exist for each expression. As there is no stats data, each of them would crash. At least one needs to be specified when creating an extstat object. 2) When loading statistics, statext_expressions_load() missed that some elements in the pg_statistics array could be NULL. This issue can only be triggered if we have some invalid data stored in the catalogs. This issue can be triggered on any branches with a typanalyze callback that returns true, where stats_valid is set to false when computing the stats (all the in-core callbacks set this flag to true, nobody in their right mind would do that except me here, I suspect). The restore of extended stats for expressions makes this defect more easily reachable by *bypassing* the build, but as the previous sentence describes it is *not* a mandatory requirement depending on what a typanalyze callback does. Hence, we have patch it in all the stable branches anyway. The code allows NULL data to exist for some expressions, but the load does not cope with it. This is reflected by fixing two code paths:=20 2-1) statext_expressions_load() needs to be able to load NULL data. 2-2) examine_variable() in selfuncs.c needs to lead with this possible consequence. All the callers of examine_variable() have an exit path in selfuncs.c if there is no stats data available, assuming some defaults, in the event where statsTuple is NULL (aka invalid). Note that it is possible to reach this path with a typanalyze that returns true, meaning that some data is available and that we store NULL data to be stored, but the compute_stats callback has the idea to set stats_valid to false. We never set stats_valid to false in any of the in-core callbacks. In order to demonstrate these two bugs, I have implemented a test module called test_custom_types, as of 0002 (btree operator bloats the module, it is required for extended stats), that includes a simple custom type with two buggy typanalyze callbacks (one for the build defect, and one for the load defect). I think that it would be a good thing to backpatch this module with the main fix, so as we can cover these fancy scenarios for all released branches. This could be enlarged for more cases if we have more defects detected in the future in this area of the code. This affects versions down to v15. In order to finish the business with the restore of extended stats for this release, these defects have to be addressed first. It does not impact what has been already committed for the restore of extended stats, fortunately: we have not touched the expressions yet and the patch is still floating around in this CF. I am registering this item in the final CF, as a bug fix. The typanalyze requirements may sound it like something worth only fixing on HEAD, but I don't really see a reason why back-branches should not be fixed as well.=20 So, thoughts or comments?=20 -- Michael --0EXhyj2swSESoM8i Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v1-0001-Fix-two-defects-with-extended-statistics-for-expr.patch Content-Transfer-Encoding: quoted-printable =46rom 69e219ef17c5100911851dcba093ba211720771d Mon Sep 17 00:00:00 2001 =46rom: Michael Paquier Date: Fri, 27 Feb 2026 09:36:59 +0900 Subject: [PATCH v1 1/2] Fix two defects with extended statistics for expressions This commit addresses two pointer dereferences that are reachable in the extended statistics code: 1) When building the statistics, the examination of an expression through examine_attribute() missed the fact that this routine could return NULL if a typanalyze function triggers its give-up path (typanalyze callback returns false, no rows or no stats computed). The build of statistics for MCV, dependencies, ndistinct and expressions would have tried to look at a NULL pointer, crashing the server. 2) When loading extended statistics for expressions, statext_expressions_load() has never considered the fact that an element of the pg_statistic array, storing the statistics of an expression, could store a NULL entry. Such entries can be generated for example on an expression whose statistics could not be gathered. While these conditions cannot be reached with the in-core typanalyze callbacks, it is possible to trigger these issues with at least a custom data with a typanalyze callback. Attribute statistics offer already similar protections than the ones added in this commit. --- src/backend/statistics/extended_stats.c | 20 ++++++++++++++++++++ src/backend/utils/adt/selfuncs.c | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statisti= cs/extended_stats.c index 3895ed72ef75..334c64985814 100644 --- a/src/backend/statistics/extended_stats.c +++ b/src/backend/statistics/extended_stats.c @@ -736,6 +736,16 @@ lookup_var_attr_stats(Bitmapset *attrs, List *exprs, =20 stats[i] =3D examine_attribute(expr); =20 + /* + * If the expression has been found as non-analyzable, give up. We + * will not be able to build extended stats with it. + */ + if (stats[i] =3D=3D NULL) + { + pfree(stats); + return NULL; + } + /* * XXX We need tuple descriptor later, and we just grab it from * stats[0]->tupDesc (see e.g. statext_mcv_build). But as coded @@ -2396,6 +2406,9 @@ serialize_expr_stats(AnlExprData *exprdata, int nexpr= s) /* * Loads pg_statistic record from expression statistics for expression * identified by the supplied index. + * + * Returns the pg_statistic record found, or NULL if there is no statistics + * data to use. */ HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx) @@ -2424,6 +2437,13 @@ statext_expressions_load(Oid stxoid, bool inh, int i= dx) =20 deconstruct_expanded_array(eah); =20 + if (eah->dnulls && eah->dnulls[idx]) + { + /* No data found for this expression, give up. */ + ReleaseSysCache(htup); + return NULL; + } + td =3D DatumGetHeapTupleHeader(eah->dvalues[idx]); =20 /* Build a temporary HeapTuple control structure */ diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfu= ncs.c index 29fec6555938..587edfc6e839 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -5923,7 +5923,11 @@ examine_variable(PlannerInfo *root, Node *node, int = varRelid, vardata->statsTuple =3D statext_expressions_load(info->statOid, rte->inh, pos); =20 - vardata->freefunc =3D ReleaseDummy; + /* Nothing to release if no data found */ + if (vardata->statsTuple !=3D NULL) + { + vardata->freefunc =3D ReleaseDummy; + } =20 /* * Test if user has permission to access all rows from the --=20 2.53.0 --0EXhyj2swSESoM8i Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v1-0002-test_custom_types-Test-module-for-custom-data-typ.patch Content-Transfer-Encoding: quoted-printable =46rom 303b207e41805893336bbc4cbfcb41458c4131f1 Mon Sep 17 00:00:00 2001 =46rom: Michael Paquier Date: Fri, 27 Feb 2026 09:37:58 +0900 Subject: [PATCH v1 2/2] test_custom_types: Test module for custom data types This includes a new test module for a custom data type, that currently includes dummy typanalyze callbacks to test patterns related to the computation of statistics with ANALYZE. --- src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + src/test/modules/test_custom_types/.gitignore | 4 + src/test/modules/test_custom_types/Makefile | 20 ++ src/test/modules/test_custom_types/README | 9 + .../expected/test_custom_types.out | 160 +++++++++++++++ .../modules/test_custom_types/meson.build | 33 ++++ .../sql/test_custom_types.sql | 84 ++++++++ .../test_custom_types--1.0.sql | 164 ++++++++++++++++ .../test_custom_types/test_custom_types.c | 182 ++++++++++++++++++ .../test_custom_types.control | 5 + 11 files changed, 663 insertions(+) create mode 100644 src/test/modules/test_custom_types/.gitignore create mode 100644 src/test/modules/test_custom_types/Makefile create mode 100644 src/test/modules/test_custom_types/README create mode 100644 src/test/modules/test_custom_types/expected/test_custom= _types.out create mode 100644 src/test/modules/test_custom_types/meson.build create mode 100644 src/test/modules/test_custom_types/sql/test_custom_type= s.sql create mode 100644 src/test/modules/test_custom_types/test_custom_types--1= =2E0.sql create mode 100644 src/test/modules/test_custom_types/test_custom_types.c create mode 100644 src/test/modules/test_custom_types/test_custom_types.co= ntrol diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 44c7163c1cd5..4ac5c84db439 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -23,6 +23,7 @@ SUBDIRS =3D \ test_copy_callbacks \ test_custom_rmgrs \ test_custom_stats \ + test_custom_types \ test_ddl_deparse \ test_dsa \ test_dsm_registry \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 2634a519935a..e2b3eef41368 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -24,6 +24,7 @@ subdir('test_copy_callbacks') subdir('test_cplusplusext') subdir('test_custom_rmgrs') subdir('test_custom_stats') +subdir('test_custom_types') subdir('test_ddl_deparse') subdir('test_dsa') subdir('test_dsm_registry') diff --git a/src/test/modules/test_custom_types/.gitignore b/src/test/modul= es/test_custom_types/.gitignore new file mode 100644 index 000000000000..5dcb3ff97235 --- /dev/null +++ b/src/test/modules/test_custom_types/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_custom_types/Makefile b/src/test/modules= /test_custom_types/Makefile new file mode 100644 index 000000000000..e1b582b26ea3 --- /dev/null +++ b/src/test/modules/test_custom_types/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/test_custom_types/Makefile + +MODULES =3D test_custom_types + +EXTENSION =3D test_custom_types +DATA =3D test_custom_types--1.0.sql +PGFILEDESC =3D "test_custom_types - tests for dummy custom types" + +REGRESS =3D test_custom_types + +ifdef USE_PGXS +PG_CONFIG =3D pg_config +PGXS :=3D $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir =3D src/test/modules/test_custom_types +top_builddir =3D ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_custom_types/README b/src/test/modules/t= est_custom_types/README new file mode 100644 index 000000000000..a37d2db577ea --- /dev/null +++ b/src/test/modules/test_custom_types/README @@ -0,0 +1,9 @@ +test_custom_types +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +This module contains a set of custom data types, with some of the following +patterns: + +- typanalyze function registered to a custom type, returning false. +- typanalyze function registered to a custom type, registering invalid sta= ts + data. 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 new file mode 100644 index 000000000000..43a857081ef9 --- /dev/null +++ b/src/test/modules/test_custom_types/expected/test_custom_types.out @@ -0,0 +1,160 @@ +-- Test with various custom types +CREATE EXTENSION test_custom_types; +-- Test comparison functions +SELECT '42'::int_custom =3D '42'::int_custom AS eq_test; + eq_test=20 +--------- + t +(1 row) + +SELECT '42'::int_custom <> '42'::int_custom AS nt_test; + nt_test=20 +--------- + f +(1 row) + +SELECT '42'::int_custom < '100'::int_custom AS lt_test; + lt_test=20 +--------- + t +(1 row) + +SELECT '100'::int_custom > '42'::int_custom AS gt_test; + gt_test=20 +--------- + t +(1 row) + +SELECT '42'::int_custom <=3D '100'::int_custom AS le_test; + le_test=20 +--------- + t +(1 row) + +SELECT '100'::int_custom >=3D '42'::int_custom AS ge_test; + ge_test=20 +--------- + t +(1 row) + +-- Create a table with the int_custom type +CREATE TABLE test_table ( + id int, + data int_custom +); +INSERT INTO test_table VALUES (1, '42'), (2, '100'), (3, '200'); +-- Verify data was inserted correctly +SELECT * FROM test_table ORDER BY id; + id | data=20 +----+------ + 1 | 42 + 2 | 100 + 3 | 200 +(3 rows) + +-- Dummy function used for expression evaluations. +CREATE OR REPLACE FUNCTION func_int_custom (p_value int_custom) + RETURNS int_custom LANGUAGE plpgsql AS $$ + BEGIN + RETURN p_value; + END; $$; +-- Switch type to use typanalyze function that always returns false. +ALTER TYPE int_custom SET (ANALYZE =3D int_custom_typanalyze_false); +-- Extended statistics with an attribute that cannot be analyzed. +-- This includes all statistics kinds. +CREATE STATISTICS test_stats ON 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 =3D d.stxoid + WHERE stxname =3D 'test_stats'; + stxname | expr_stats_is_null=20 +------------+-------------------- + test_stats | t +(1 row) + +DROP STATISTICS test_stats; +-- Extended statistics with an expression that cannot be analyzed. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_tabl= e; +-- 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 =3D d.stxoid + WHERE stxname =3D 'test_stats'; + stxname | expr_stats_is_null=20 +------------+-------------------- + test_stats | t +(1 row) + +DROP STATISTICS test_stats; +-- Switch type to use typanalyze function that generates invalid data. +ALTER TYPE int_custom SET (ANALYZE =3D int_custom_typanalyze_invalid); +-- Extended statistics with an attribute that generates invalid stats. +CREATE STATISTICS test_stats ON 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 =3D d.stxoid + WHERE stxname =3D 'test_stats'; + stxname | expr_stats_is_null=20 +------------+-------------------- + test_stats | t +(1 row) + +DROP STATISTICS test_stats; +-- Extended statistics with an expression that generates invalid data. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_tabl= e; +-- Computation of the stats fails, some 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 =3D d.stxoid + WHERE stxname =3D 'test_stats'; + stxname | expr_stats_is_null=20 +------------+-------------------- + test_stats | f +(1 row) + +-- There should be some data stored for the expression, stored as NULL. +SELECT * FROM pg_stats_ext_exprs WHERE statistics_name =3D 'test_stats' \gx +-[ RECORD 1 ]----------+---------------------- +schemaname | public +tablename | test_table +statistics_schemaname | public +statistics_name | test_stats +statistics_owner | ioltas +expr | func_int_custom(data) +inherited | f +null_frac |=20 +avg_width |=20 +n_distinct |=20 +most_common_vals |=20 +most_common_freqs |=20 +histogram_bounds |=20 +correlation |=20 +most_common_elems |=20 +most_common_elem_freqs |=20 +elem_count_histogram |=20 +range_length_histogram |=20 +range_empty_frac |=20 +range_bounds_histogram |=20 + +-- Run a query able to load the extended stats, including the NULL data. +SELECT COUNT(*) FROM test_table GROUP BY (func_int_custom(data)); + count=20 +------- + 1 + 1 + 1 +(3 rows) + +DROP STATISTICS test_stats; +-- Cleanup +DROP FUNCTION func_int_custom; +DROP TABLE test_table; +DROP EXTENSION test_custom_types; diff --git a/src/test/modules/test_custom_types/meson.build b/src/test/modu= les/test_custom_types/meson.build new file mode 100644 index 000000000000..98f58f3547fb --- /dev/null +++ b/src/test/modules/test_custom_types/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +test_custom_types_sources =3D files( + 'test_custom_types.c', +) + +if host_system =3D=3D 'windows' + test_custom_types_sources +=3D rc_lib_gen.process(win32ver_rc, extra_arg= s: [ + '--NAME', 'test_custom_types', + '--FILEDESC', 'test_custom_types - test for dummy custom types',]) +endif + +test_custom_types =3D shared_module('test_custom_types', + test_custom_types_sources, + kwargs: pg_test_mod_args, +) +test_install_libs +=3D test_custom_types + +test_install_data +=3D files( + 'test_custom_types.control', + 'test_custom_types--1.0.sql', +) + +tests +=3D { + 'name': 'test_custom_types', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_custom_types', + ], + }, +} 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 new file mode 100644 index 000000000000..2cde1917f3bb --- /dev/null +++ b/src/test/modules/test_custom_types/sql/test_custom_types.sql @@ -0,0 +1,84 @@ +-- Test with various custom types + +CREATE EXTENSION test_custom_types; + +-- Test comparison functions +SELECT '42'::int_custom =3D '42'::int_custom AS eq_test; +SELECT '42'::int_custom <> '42'::int_custom AS nt_test; +SELECT '42'::int_custom < '100'::int_custom AS lt_test; +SELECT '100'::int_custom > '42'::int_custom AS gt_test; +SELECT '42'::int_custom <=3D '100'::int_custom AS le_test; +SELECT '100'::int_custom >=3D '42'::int_custom AS ge_test; + +-- Create a table with the int_custom type +CREATE TABLE test_table ( + id int, + data int_custom +); +INSERT INTO test_table VALUES (1, '42'), (2, '100'), (3, '200'); + +-- Verify data was inserted correctly +SELECT * FROM test_table ORDER BY id; + +-- Dummy function used for expression evaluations. +CREATE OR REPLACE FUNCTION func_int_custom (p_value int_custom) + RETURNS int_custom LANGUAGE plpgsql AS $$ + BEGIN + RETURN p_value; + END; $$; + +-- Switch type to use typanalyze function that always returns false. +ALTER TYPE int_custom SET (ANALYZE =3D int_custom_typanalyze_false); + +-- Extended statistics with an attribute that cannot be analyzed. +-- This includes all statistics kinds. +CREATE STATISTICS test_stats ON 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 =3D d.stxoid + WHERE stxname =3D 'test_stats'; +DROP STATISTICS test_stats; + +-- Extended statistics with an expression that cannot be analyzed. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_tabl= e; +-- 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 =3D d.stxoid + WHERE stxname =3D 'test_stats'; +DROP STATISTICS test_stats; + +-- Switch type to use typanalyze function that generates invalid data. +ALTER TYPE int_custom SET (ANALYZE =3D int_custom_typanalyze_invalid); + +-- Extended statistics with an attribute that generates invalid stats. +CREATE STATISTICS test_stats ON 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 =3D d.stxoid + WHERE stxname =3D 'test_stats'; +DROP STATISTICS test_stats; + +-- Extended statistics with an expression that generates invalid data. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_tabl= e; +-- Computation of the stats fails, some 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 =3D d.stxoid + WHERE stxname =3D 'test_stats'; +-- There should be some data stored for the expression, stored as NULL. +SELECT * FROM pg_stats_ext_exprs WHERE statistics_name =3D 'test_stats' \gx +-- Run a query able to load the extended stats, including the NULL data. +SELECT COUNT(*) FROM test_table GROUP BY (func_int_custom(data)); +DROP STATISTICS test_stats; + +-- Cleanup +DROP FUNCTION func_int_custom; +DROP TABLE test_table; +DROP EXTENSION test_custom_types; 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 new file mode 100644 index 000000000000..ce0e905d6364 --- /dev/null +++ b/src/test/modules/test_custom_types/test_custom_types--1.0.sql @@ -0,0 +1,164 @@ +/* src/test/modules/test_custom_types/test_custom_types--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_custom_types" to load this file. \quit + +-- +-- Input/output functions for int_custom type +-- +CREATE FUNCTION int_custom_in(cstring) +RETURNS int_custom +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_out(int_custom) +RETURNS cstring +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +-- +-- Typanalyze function that returns false +-- +CREATE FUNCTION int_custom_typanalyze_false(internal) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- +-- Typanalyze function that returns invalid stats +-- +CREATE FUNCTION int_custom_typanalyze_invalid(internal) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- +-- The int_custom type definition +-- +-- This type is identical to int4 in storage, and is used in subsequent +-- tests to have different properties. +-- +CREATE TYPE int_custom ( + INPUT =3D int_custom_in, + OUTPUT =3D int_custom_out, + LIKE =3D int4 +); + +-- +-- Comparison functions for int_custom +-- +-- These are required to create a btree operator class, which is needed +-- for the type to be usable in extended statistics objects. +-- +CREATE FUNCTION int_custom_eq(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_ne(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_lt(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_le(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_gt(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_ge(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_cmp(int_custom, int_custom) +RETURNS integer +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +-- Operators for int_custom, for btree operator class +CREATE OPERATOR =3D ( + LEFTARG =3D int_custom, + RIGHTARG =3D int_custom, + FUNCTION =3D int_custom_eq, + COMMUTATOR =3D =3D, + NEGATOR =3D <>, + RESTRICT =3D eqsel, + JOIN =3D eqjoinsel, + HASHES, + MERGES +); + +CREATE OPERATOR <> ( + LEFTARG =3D int_custom, + RIGHTARG =3D int_custom, + FUNCTION =3D int_custom_ne, + COMMUTATOR =3D <>, + NEGATOR =3D =3D, + RESTRICT =3D neqsel, + JOIN =3D neqjoinsel +); + +CREATE OPERATOR < ( + LEFTARG =3D int_custom, + RIGHTARG =3D int_custom, + FUNCTION =3D int_custom_lt, + COMMUTATOR =3D >, + NEGATOR =3D >=3D, + RESTRICT =3D scalarltsel, + JOIN =3D scalarltjoinsel +); + +CREATE OPERATOR <=3D ( + LEFTARG =3D int_custom, + RIGHTARG =3D int_custom, + FUNCTION =3D int_custom_le, + COMMUTATOR =3D >=3D, + NEGATOR =3D >, + RESTRICT =3D scalarlesel, + JOIN =3D scalarlejoinsel +); + +CREATE OPERATOR > ( + LEFTARG =3D int_custom, + RIGHTARG =3D int_custom, + FUNCTION =3D int_custom_gt, + COMMUTATOR =3D <, + NEGATOR =3D <=3D, + RESTRICT =3D scalargtsel, + JOIN =3D scalargtjoinsel +); + +CREATE OPERATOR >=3D ( + LEFTARG =3D int_custom, + RIGHTARG =3D int_custom, + FUNCTION =3D int_custom_ge, + COMMUTATOR =3D <=3D, + NEGATOR =3D <, + RESTRICT =3D scalargesel, + JOIN =3D scalargejoinsel +); + +-- +-- Btree operator class for int_custom +-- +-- This is required for the type to be usable in extended statistics objec= ts, +-- for attributes and expressions. +-- +CREATE OPERATOR CLASS int_custom_ops + DEFAULT FOR TYPE int_custom USING btree AS + OPERATOR 1 <, + OPERATOR 2 <=3D, + OPERATOR 3 =3D, + OPERATOR 4 >=3D, + OPERATOR 5 >, + FUNCTION 1 int_custom_cmp(int_custom, int_custom); diff --git a/src/test/modules/test_custom_types/test_custom_types.c b/src/t= est/modules/test_custom_types/test_custom_types.c new file mode 100644 index 000000000000..b5ad67e75c9f --- /dev/null +++ b/src/test/modules/test_custom_types/test_custom_types.c @@ -0,0 +1,182 @@ +/*------------------------------------------------------------------------= -- + * + * test_custom_types.c + * Test module for a set of functions for custom types. + * + * The custom type used in this module is similar to int4 for simplicity, + * except that it is able to use various typanalyze functions to enforce + * check patterns with ANALYZE. + * + * Copyright (c) 1996-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_custom_types/test_custom_types.c + * + *------------------------------------------------------------------------= -- + */ + +#include "postgres.h" + +#include "fmgr.h" +#include "commands/vacuum.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + +/* Function declarations */ +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_eq); +PG_FUNCTION_INFO_V1(int_custom_ne); +PG_FUNCTION_INFO_V1(int_custom_lt); +PG_FUNCTION_INFO_V1(int_custom_le); +PG_FUNCTION_INFO_V1(int_custom_gt); +PG_FUNCTION_INFO_V1(int_custom_ge); +PG_FUNCTION_INFO_V1(int_custom_cmp); + +/* + * int_custom_in - input function for int_custom type + * + * Converts a string to a int_custom (which is just an int32 internally). + */ +Datum +int_custom_in(PG_FUNCTION_ARGS) +{ + char *num =3D PG_GETARG_CSTRING(0); + + PG_RETURN_INT32(pg_strtoint32_safe(num, fcinfo->context)); +} + +/* + * int_custom_out - output function for int_custom type + * + * Converts a int_custom to a string. + */ +Datum +int_custom_out(PG_FUNCTION_ARGS) +{ + int32 arg1 =3D PG_GETARG_INT32(0); + char *result =3D (char *) palloc(12); /* sign, 10 digits, '\0' */ + + pg_ltoa(arg1, result); + PG_RETURN_CSTRING(result); +} + +/* + * int_custom_typanalyze_false - typanalyze function that returns false + * + * This function returns false, to simulate a type that cannot be analyzed. + */ +Datum +int_custom_typanalyze_false(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(false); +} + +/* + * Callback used to compute invalid statistics. + */ +static void +int_custom_invalid_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfu= nc, + int samplerows, double totalrows) +{ + /* We are not valid, and do not want to be. */ + stats->stats_valid =3D false; +} + +/* + * int_custom_typanalyze_invalid + * + * This function returns sets some invalid stats data, letting the caller = know + * that we are safe for an analyze, returning true. + */ +Datum +int_custom_typanalyze_invalid(PG_FUNCTION_ARGS) +{ + VacAttrStats *stats =3D (VacAttrStats *) PG_GETARG_POINTER(0); + + /* If the attstattarget column is negative, use the default value */ + if (stats->attstattarget < 0) + stats->attstattarget =3D default_statistics_target; + + /* Buggy number, no need to care as long as it is positive */ + stats->minrows =3D 300; + + /* Set callback to compute some invalid stats */ + stats->compute_stats =3D int_custom_invalid_stats; + + PG_RETURN_BOOL(true); +} + +/* + * Comparison functions for int_custom type + */ +Datum +int_custom_eq(PG_FUNCTION_ARGS) +{ + int32 arg1 =3D PG_GETARG_INT32(0); + int32 arg2 =3D PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 =3D=3D arg2); +} + +Datum +int_custom_ne(PG_FUNCTION_ARGS) +{ + int32 arg1 =3D PG_GETARG_INT32(0); + int32 arg2 =3D PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 !=3D arg2); +} + +Datum +int_custom_lt(PG_FUNCTION_ARGS) +{ + int32 arg1 =3D PG_GETARG_INT32(0); + int32 arg2 =3D PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 < arg2); +} + +Datum +int_custom_le(PG_FUNCTION_ARGS) +{ + int32 arg1 =3D PG_GETARG_INT32(0); + int32 arg2 =3D PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 <=3D arg2); +} + +Datum +int_custom_gt(PG_FUNCTION_ARGS) +{ + int32 arg1 =3D PG_GETARG_INT32(0); + int32 arg2 =3D PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 > arg2); +} + +Datum +int_custom_ge(PG_FUNCTION_ARGS) +{ + int32 arg1 =3D PG_GETARG_INT32(0); + int32 arg2 =3D PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 >=3D arg2); +} + +Datum +int_custom_cmp(PG_FUNCTION_ARGS) +{ + int32 arg1 =3D PG_GETARG_INT32(0); + int32 arg2 =3D PG_GETARG_INT32(1); + + if (arg1 < arg2) + PG_RETURN_INT32(-1); + else if (arg1 > arg2) + PG_RETURN_INT32(1); + else + PG_RETURN_INT32(0); +} diff --git a/src/test/modules/test_custom_types/test_custom_types.control b= /src/test/modules/test_custom_types/test_custom_types.control new file mode 100644 index 000000000000..1e45ded43aca --- /dev/null +++ b/src/test/modules/test_custom_types/test_custom_types.control @@ -0,0 +1,5 @@ +# test_custom_types extension +comment =3D 'Test for dummy custom types' +default_version =3D '1.0' +module_pathname =3D '$libdir/test_custom_types' +relocatable =3D true --=20 2.53.0 --0EXhyj2swSESoM8i-- --417ghOodyzBYRSAC Content-Type: application/pgp-signature; name=signature.asc -----BEGIN PGP SIGNATURE----- iQIzBAEBCgAdFiEEG72nH6vTowiyblFKnvQgOdbyQH0FAmmg6yYACgkQnvQgOdby QH3IlxAAnZMToGV+PTb8doNMpx8ZGpwtjLN/bhv95C+YGH4XYFVZzmuMY19+V6+H XXA0pgYRThYzGfA/010flyd+P99RvhP8iREzOOqpUGB9wmBBH2ympNufF/qNrnai kJj+zGfNa5rcqY/QSJ8D4a/Frr+uTE2M4r/ReLwRRu7/DLLrPmCvEPm0X2eYo11c Lj9eQzKTMfQCgMd+RQr3ZPTwJX+bchmcifxWaa/+MAIopICUGZQ/S87rzukeS9ae Q7ySsxB6bQkqXwUjao8YWcABFZV/InDEsn+G3ydTPqT35cqklDBf5+eq1HPtGVtm EduyyS9pRTwhVaK/UjWOOCSdNvuWoJddeW3zisiNJeLvGe85ATpx5jHHmfH26DOC sc6cytPsQHxHSbAovFOh585MoQEf9cPR1Ye+m8HVeLNPn920MfdwXFqT/1fAhEEE ZM080z5m115xz+Zi6qil+Z75QCQr2pptrS1q2wAhnW3sxkdOh61f7C7pOXe27wE9 kG/FtqTMaV4TVhZa90y+s4JybV6LKKWCvGEB7zWNPZK3tz/OW7Cl8DaqPaoaD8k5 /GRyso8E8SzahhfpYR5f7TJ29nO3rCFWEiwcWxadgM5wFRFRn1gvcFcY4EjTLCy9 an+eo5tZFBRUvO2tK8x4ZQ1zAzcKIxc9IH2paSVB9jFx9PKI1QI= =EBmF -----END PGP SIGNATURE----- --417ghOodyzBYRSAC--