On Thu, Feb 26, 2026 at 10:52:48PM -0500, Corey Huinker wrote:
> Patch applies for me, but there seems to be some user-specific stuff in the
> test, which causes it to fail:

Yep.  I've noticed that in the CI a few minutes ago.  I have switched
the tests to use a query where the owner does not show up, leading to
the same coverage without the user-dependency blip.  I have checked
that this version cools down the CI.

> A nitpick about the test - it uses a plpgsql function when we've been
> moving such trivial functions to SQL standard function bodies for a while
> now, and they were introduced back in v14 so there's no backporting
> concern.

No, that's on purpose.  Using a SQL function with a body would not
trigger the problem with the stats loaded at the end of the SQL test
as we would skip the fatal call of statext_expressions_load().  Based
on your confusion, I guess that a note to document that is in order,
at least, so as nobody comes with the idea of changing the definition
of this function..
--
Michael
From 69e219ef17c5100911851dcba093ba211720771d Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email protected]>
Date: Fri, 27 Feb 2026 09:36:59 +0900
Subject: [PATCH v2 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/statistics/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,
 
                stats[i] = examine_attribute(expr);
 
+               /*
+                * 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] == 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 nexprs)
 /*
  * 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 idx)
 
        deconstruct_expanded_array(eah);
 
+       if (eah->dnulls && eah->dnulls[idx])
+       {
+               /* No data found for this expression, give up. */
+               ReleaseSysCache(htup);
+               return NULL;
+       }
+
        td = DatumGetHeapTupleHeader(eah->dvalues[idx]);
 
        /* Build a temporary HeapTuple control structure */
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.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 =
                                                
statext_expressions_load(info->statOid, rte->inh, pos);
 
-                                       vardata->freefunc = ReleaseDummy;
+                                       /* Nothing to release if no data found 
*/
+                                       if (vardata->statsTuple != NULL)
+                                       {
+                                               vardata->freefunc = 
ReleaseDummy;
+                                       }
 
                                        /*
                                         * Test if user has permission to 
access all rows from the
-- 
2.53.0

From af28bd65f7c010d7c658186cb36b5b791ea0ff9a Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email protected]>
Date: Fri, 27 Feb 2026 13:23:21 +0900
Subject: [PATCH v2 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            | 172 +++++++++++++++++
 .../modules/test_custom_types/meson.build     |  33 ++++
 .../sql/test_custom_types.sql                 | 101 ++++++++++
 .../test_custom_types--1.0.sql                | 164 ++++++++++++++++
 .../test_custom_types/test_custom_types.c     | 182 ++++++++++++++++++
 .../test_custom_types.control                 |   5 +
 11 files changed, 692 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_types.sql
 create mode 100644 
src/test/modules/test_custom_types/test_custom_types--1.0.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.control

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 = \
                  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/modules/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 = test_custom_types
+
+EXTENSION = test_custom_types
+DATA = test_custom_types--1.0.sql
+PGFILEDESC = "test_custom_types - tests for dummy custom types"
+
+REGRESS = test_custom_types
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_custom_types
+top_builddir = ../../../..
+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/test_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
+=================
+
+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 stats
+  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..113bdace588e
--- /dev/null
+++ b/src/test/modules/test_custom_types/expected/test_custom_types.out
@@ -0,0 +1,172 @@
+-- Test with various custom types
+CREATE EXTENSION test_custom_types;
+-- Test comparison functions
+SELECT '42'::int_custom = '42'::int_custom AS eq_test;
+ eq_test 
+---------
+ t
+(1 row)
+
+SELECT '42'::int_custom <> '42'::int_custom AS nt_test;
+ nt_test 
+---------
+ f
+(1 row)
+
+SELECT '42'::int_custom < '100'::int_custom AS lt_test;
+ lt_test 
+---------
+ t
+(1 row)
+
+SELECT '100'::int_custom > '42'::int_custom AS gt_test;
+ gt_test 
+---------
+ t
+(1 row)
+
+SELECT '42'::int_custom <= '100'::int_custom AS le_test;
+ le_test 
+---------
+ t
+(1 row)
+
+SELECT '100'::int_custom >= '42'::int_custom AS ge_test;
+ ge_test 
+---------
+ 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 
+----+------
+  1 | 42
+  2 | 100
+  3 | 200
+(3 rows)
+
+-- Dummy function used for expression evaluations.
+-- Note that this function does not use a function body on purpose, so as
+-- external statistics can be loaded from it.
+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 = 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 = d.stxoid
+  WHERE stxname = 'test_stats';
+  stxname   | expr_stats_is_null 
+------------+--------------------
+ 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_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;
+-- Switch type to use typanalyze function that generates invalid data.
+ALTER TYPE int_custom SET (ANALYZE = 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 = d.stxoid
+  WHERE stxname = 'test_stats';
+  stxname   | expr_stats_is_null 
+------------+--------------------
+ 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_table;
+-- 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 = d.stxoid
+  WHERE stxname = 'test_stats';
+  stxname   | expr_stats_is_null 
+------------+--------------------
+ test_stats | f
+(1 row)
+
+-- There should be some data stored for the expression, stored as NULL.
+SELECT tablename,
+  statistics_name,
+  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
+  FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx
+-[ RECORD 1 ]----------+-----------
+tablename              | test_table
+statistics_name        | test_stats
+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 | 
+
+-- 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 
+-------
+     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/modules/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 = files(
+  'test_custom_types.c',
+)
+
+if host_system == 'windows'
+  test_custom_types_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_custom_types',
+    '--FILEDESC', 'test_custom_types - test for dummy custom types',])
+endif
+
+test_custom_types = shared_module('test_custom_types',
+  test_custom_types_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_custom_types
+
+test_install_data += files(
+  'test_custom_types.control',
+  'test_custom_types--1.0.sql',
+)
+
+tests += {
+  '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..59147217ea02
--- /dev/null
+++ b/src/test/modules/test_custom_types/sql/test_custom_types.sql
@@ -0,0 +1,101 @@
+-- Test with various custom types
+
+CREATE EXTENSION test_custom_types;
+
+-- Test comparison functions
+SELECT '42'::int_custom = '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 <= '100'::int_custom AS le_test;
+SELECT '100'::int_custom >= '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.
+-- Note that this function does not use a function body on purpose, so as
+-- external statistics can be loaded from it.
+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 = 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 = d.stxoid
+  WHERE stxname = '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_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;
+
+-- Switch type to use typanalyze function that generates invalid data.
+ALTER TYPE int_custom SET (ANALYZE = 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 = d.stxoid
+  WHERE stxname = '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_table;
+-- 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 = d.stxoid
+  WHERE stxname = 'test_stats';
+-- There should be some data stored for the expression, stored as NULL.
+SELECT tablename,
+  statistics_name,
+  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
+  FROM pg_stats_ext_exprs WHERE statistics_name = '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 = int_custom_in,
+  OUTPUT = int_custom_out,
+  LIKE = 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 = (
+  LEFTARG = int_custom,
+  RIGHTARG = int_custom,
+  FUNCTION = int_custom_eq,
+  COMMUTATOR = =,
+  NEGATOR = <>,
+  RESTRICT = eqsel,
+  JOIN = eqjoinsel,
+  HASHES,
+  MERGES
+);
+
+CREATE OPERATOR <> (
+  LEFTARG = int_custom,
+  RIGHTARG = int_custom,
+  FUNCTION = int_custom_ne,
+  COMMUTATOR = <>,
+  NEGATOR = =,
+  RESTRICT = neqsel,
+  JOIN = neqjoinsel
+);
+
+CREATE OPERATOR < (
+  LEFTARG = int_custom,
+  RIGHTARG = int_custom,
+  FUNCTION = int_custom_lt,
+  COMMUTATOR = >,
+  NEGATOR = >=,
+  RESTRICT = scalarltsel,
+  JOIN = scalarltjoinsel
+);
+
+CREATE OPERATOR <= (
+  LEFTARG = int_custom,
+  RIGHTARG = int_custom,
+  FUNCTION = int_custom_le,
+  COMMUTATOR = >=,
+  NEGATOR = >,
+  RESTRICT = scalarlesel,
+  JOIN = scalarlejoinsel
+);
+
+CREATE OPERATOR > (
+  LEFTARG = int_custom,
+  RIGHTARG = int_custom,
+  FUNCTION = int_custom_gt,
+  COMMUTATOR = <,
+  NEGATOR = <=,
+  RESTRICT = scalargtsel,
+  JOIN = scalargtjoinsel
+);
+
+CREATE OPERATOR >= (
+  LEFTARG = int_custom,
+  RIGHTARG = int_custom,
+  FUNCTION = int_custom_ge,
+  COMMUTATOR = <=,
+  NEGATOR = <,
+  RESTRICT = scalargesel,
+  JOIN = scalargejoinsel
+);
+
+--
+-- Btree operator class for int_custom
+--
+-- This is required for the type to be usable in extended statistics objects,
+-- for attributes and expressions.
+--
+CREATE OPERATOR CLASS int_custom_ops
+  DEFAULT FOR TYPE int_custom USING btree AS
+    OPERATOR    1     <,
+    OPERATOR    2     <=,
+    OPERATOR    3     =,
+    OPERATOR    4     >=,
+    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/test/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 = 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 = PG_GETARG_INT32(0);
+       char       *result = (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 fetchfunc,
+                                                int samplerows, double 
totalrows)
+{
+       /* We are not valid, and do not want to be. */
+       stats->stats_valid = 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 = (VacAttrStats *) PG_GETARG_POINTER(0);
+
+       /* If the attstattarget column is negative, use the default value */
+       if (stats->attstattarget < 0)
+               stats->attstattarget = default_statistics_target;
+
+       /* Buggy number, no need to care as long as it is positive */
+       stats->minrows = 300;
+
+       /* Set callback to compute some invalid stats */
+       stats->compute_stats = int_custom_invalid_stats;
+
+       PG_RETURN_BOOL(true);
+}
+
+/*
+ * Comparison functions for int_custom type
+ */
+Datum
+int_custom_eq(PG_FUNCTION_ARGS)
+{
+       int32           arg1 = PG_GETARG_INT32(0);
+       int32           arg2 = PG_GETARG_INT32(1);
+
+       PG_RETURN_BOOL(arg1 == arg2);
+}
+
+Datum
+int_custom_ne(PG_FUNCTION_ARGS)
+{
+       int32           arg1 = PG_GETARG_INT32(0);
+       int32           arg2 = PG_GETARG_INT32(1);
+
+       PG_RETURN_BOOL(arg1 != arg2);
+}
+
+Datum
+int_custom_lt(PG_FUNCTION_ARGS)
+{
+       int32           arg1 = PG_GETARG_INT32(0);
+       int32           arg2 = PG_GETARG_INT32(1);
+
+       PG_RETURN_BOOL(arg1 < arg2);
+}
+
+Datum
+int_custom_le(PG_FUNCTION_ARGS)
+{
+       int32           arg1 = PG_GETARG_INT32(0);
+       int32           arg2 = PG_GETARG_INT32(1);
+
+       PG_RETURN_BOOL(arg1 <= arg2);
+}
+
+Datum
+int_custom_gt(PG_FUNCTION_ARGS)
+{
+       int32           arg1 = PG_GETARG_INT32(0);
+       int32           arg2 = PG_GETARG_INT32(1);
+
+       PG_RETURN_BOOL(arg1 > arg2);
+}
+
+Datum
+int_custom_ge(PG_FUNCTION_ARGS)
+{
+       int32           arg1 = PG_GETARG_INT32(0);
+       int32           arg2 = PG_GETARG_INT32(1);
+
+       PG_RETURN_BOOL(arg1 >= arg2);
+}
+
+Datum
+int_custom_cmp(PG_FUNCTION_ARGS)
+{
+       int32           arg1 = PG_GETARG_INT32(0);
+       int32           arg2 = 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 = 'Test for dummy custom types'
+default_version = '1.0'
+module_pathname = '$libdir/test_custom_types'
+relocatable = true
-- 
2.53.0

Attachment: signature.asc
Description: PGP signature

Reply via email to