test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
+ test_custom_types \
test_ddl_deparse \
test_dsa \
test_dsm_registry \
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')
--- /dev/null
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
--- /dev/null
+# 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
--- /dev/null
+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.
--- /dev/null
+-- 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,
+ 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;
--- /dev/null
+# 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',
+ ],
+ },
+}
--- /dev/null
+-- 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,
+ 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;
--- /dev/null
+/* 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);
--- /dev/null
+/*--------------------------------------------------------------------------
+ *
+ * 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 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);
+}
--- /dev/null
+# test_custom_types extension
+comment = 'Tests for dummy custom types'
+default_version = '1.0'
+module_pathname = '$libdir/test_custom_types'
+relocatable = true