test_custom_types: Test module with fancy custom data types

This commit adds a new test module called "test_custom_types", that can
be used to stress code paths related to custom data type
implementations.

Currently, this is used as a test suite to validate the set of fixes
done in 3b7a6fa157, that requires some typanalyze callbacks that can
force very specific backend behaviors, as of:
- typanalyze callback that returns "false" as status, to mark a failure
in computing statistics.
- typanalyze callback that returns "true" but let's the backend know
that no interesting stats could be computed, with stats_valid set to
"false".

This could be extended more in the future if more problems are found.
For simplicity, the module uses a fake int4 data type, that requires a
btree operator class to be usable with extended statistics.  The type is
created by the extension, and its properties are altered in the test.

Like 3b7a6fa157, this module is backpatched down to v14, for coverage
purposes.

Author: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/aaDrJsE1I5mrE-QF@paquier.xyz
Backpatch-through: 14
This commit is contained in:
Michael Paquier 2026-03-02 11:10:39 +09:00
parent f033abc6c4
commit 8eedbc2cc4
10 changed files with 698 additions and 0 deletions

View file

@ -15,6 +15,7 @@ SUBDIRS = \
snapshot_too_old \
spgist_name_ops \
test_bloomfilter \
test_custom_types \
test_ddl_deparse \
test_escape \
test_extensions \

View file

@ -0,0 +1,4 @@
# Generated subdirectories
/log/
/results/
/tmp_check/

View file

@ -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

View file

@ -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.

View file

@ -0,0 +1,174 @@
-- Tests 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 SQL-standard 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;
-- There should be no data stored for the expression.
SELECT tablename,
statistics_name,
null_frac,
avg_width
FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx
(0 rows)
-- 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
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 |
-- 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;

View file

@ -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 - tests 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',
],
},
}

View file

@ -0,0 +1,104 @@
-- Tests 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 SQL-standard 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;
-- There should be no data stored for the expression.
SELECT tablename,
statistics_name,
null_frac,
avg_width
FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx
-- 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
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;

View file

@ -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);

View file

@ -0,0 +1,184 @@
/*--------------------------------------------------------------------------
*
* 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(num));
}
/*
* 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 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);
Form_pg_attribute attr = stats->attr;
/* If the attstattarget column is negative, use the default value */
/* NB: it is okay to scribble on stats->attr since it's a copy */
if (attr->attstattarget < 0)
attr->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);
}

View file

@ -0,0 +1,5 @@
# test_custom_types extension
comment = 'Tests for dummy custom types'
default_version = '1.0'
module_pathname = '$libdir/test_custom_types'
relocatable = true