From 72af9534181ff0b286905be91aa29fcc1793a4b1 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 18 Jul 2025 11:35:39 -0500
Subject: [PATCH v11 1/1] Add counters for generic and custom plan executions
 to pg_stat_statements

This patch introduces two new counters in pg_stat_statements:
- generic_plan_calls
- custom_plan_calls

These track how many times a prepared statement was executed using a
generic or custom plan, respectively.

To make the current plan cache mode available to Executor hooks (e.g.,
ExecutorEnd), a new "mode" field of type "CachedPlanMode" is added to
CachedPlan.

This mode is passed to "CreateQueryDesc", which accepts it as an argument
and stores it in a new "cached_plan_mode" field in QueryDesc. This makes
the mode accessible to the Executor via QueryDesc.

Although there are only two actual plan cache modes (custom and generic),
CachedPlanMode includes a third value to represent an unset state.
This "not set" mode can also be used by extensions in cases where the
plan cache is not involved but statistics are still relevant.
---
 contrib/pg_stat_statements/Makefile           |   3 +-
 .../pg_stat_statements/expected/plancache.out | 295 ++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |   2 +
 .../pg_stat_statements--1.12--1.13.sql        |  78 +++++
 .../pg_stat_statements/pg_stat_statements.c   |  51 ++-
 .../pg_stat_statements.control                |   2 +-
 contrib/pg_stat_statements/sql/plancache.sql  | 129 ++++++++
 doc/src/sgml/pgstatstatements.sgml            |  18 ++
 src/backend/commands/copyto.c                 |   2 +-
 src/backend/commands/createas.c               |   2 +-
 src/backend/commands/explain.c                |   7 +-
 src/backend/commands/extension.c              |   2 +-
 src/backend/commands/matview.c                |   2 +-
 src/backend/commands/prepare.c                |   2 +-
 src/backend/executor/execParallel.c           |   2 +-
 src/backend/executor/functions.c              |   3 +-
 src/backend/executor/spi.c                    |   4 +-
 src/backend/tcop/pquery.c                     |  22 +-
 src/backend/utils/cache/plancache.c           |   4 +
 src/include/commands/explain.h                |   4 +-
 src/include/executor/execdesc.h               |   7 +-
 src/include/utils/plancache.h                 |   9 +
 22 files changed, 621 insertions(+), 29 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/plancache.out
 create mode 100644 contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
 create mode 100644 contrib/pg_stat_statements/sql/plancache.sql

diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index b2bd8794d2a..996a5fac448 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -7,6 +7,7 @@ OBJS = \
 
 EXTENSION = pg_stat_statements
 DATA = pg_stat_statements--1.4.sql \
+	pg_stat_statements--1.12--1.13.sql \
 	pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \
 	pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \
 	pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \
@@ -20,7 +21,7 @@ LDFLAGS_SL += $(filter -lm, $(LIBS))
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf
 REGRESS = select dml cursors utility level_tracking planning \
 	user_activity wal entry_timestamp privileges extended \
-	parallel cleanup oldextversions squashing
+	parallel cleanup oldextversions squashing plancache
 # Disabled because these tests require "shared_preload_libraries=pg_stat_statements",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out
new file mode 100644
index 00000000000..7b05d5a425f
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/plancache.out
@@ -0,0 +1,295 @@
+--
+-- Information related to plan cache
+--
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+DEALLOCATE p1;
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+ ?column? 
+----------
+ 1
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  1 |                 2 | t        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(3 rows)
+
+\close_prepared p1
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                      query                                       
+-------+--------------------+-------------------+----------+----------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1)
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) EXECUTE p1(1)
+     6 |                  2 |                 4 | f        | PREPARE p1 AS SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+ select_one_func 
+-----------------
+ 
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                       query                        
+-------+--------------------+-------------------+----------+----------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SELECT select_one_func($1)
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(5 rows)
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+            QUERY PLAN             
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+ QUERY PLAN 
+------------
+ Result
+(1 row)
+
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+ calls | generic_plan_calls | custom_plan_calls | toplevel |                                             query                                              
+-------+--------------------+-------------------+----------+------------------------------------------------------------------------------------------------
+     3 |                  0 |                 0 | t        | CALL select_one_proc($1)
+     3 |                  0 |                 0 | t        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1)
+     6 |                  0 |                 0 | f        | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1);
+     3 |                  0 |                 0 | t        | EXPLAIN (COSTS OFF) SELECT select_one_func($1)
+     6 |                  2 |                 4 | f        | SELECT $1
+     1 |                  0 |                 0 | t        | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+     3 |                  0 |                 0 | t        | SET plan_cache_mode TO $1
+(7 rows)
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 01a6cbdcf61..110fb82fe12 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements
 install_data(
   'pg_stat_statements.control',
   'pg_stat_statements--1.4.sql',
+  'pg_stat_statements--1.12--1.13.sql',
   'pg_stat_statements--1.11--1.12.sql',
   'pg_stat_statements--1.10--1.11.sql',
   'pg_stat_statements--1.9--1.10.sql',
@@ -57,6 +58,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'plancache',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
new file mode 100644
index 00000000000..2f0eaf14ec3
--- /dev/null
+++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql
@@ -0,0 +1,78 @@
+/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements;
+ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean);
+
+/* Then we can drop them */
+DROP VIEW pg_stat_statements;
+DROP FUNCTION pg_stat_statements(boolean);
+
+/* Now redefine */
+CREATE FUNCTION pg_stat_statements(IN showtext boolean,
+    OUT userid oid,
+    OUT dbid oid,
+    OUT toplevel bool,
+    OUT queryid bigint,
+    OUT query text,
+    OUT plans int8,
+    OUT total_plan_time float8,
+    OUT min_plan_time float8,
+    OUT max_plan_time float8,
+    OUT mean_plan_time float8,
+    OUT stddev_plan_time float8,
+    OUT calls int8,
+    OUT total_exec_time float8,
+    OUT min_exec_time float8,
+    OUT max_exec_time float8,
+    OUT mean_exec_time float8,
+    OUT stddev_exec_time float8,
+    OUT rows int8,
+    OUT shared_blks_hit int8,
+    OUT shared_blks_read int8,
+    OUT shared_blks_dirtied int8,
+    OUT shared_blks_written int8,
+    OUT local_blks_hit int8,
+    OUT local_blks_read int8,
+    OUT local_blks_dirtied int8,
+    OUT local_blks_written int8,
+    OUT temp_blks_read int8,
+    OUT temp_blks_written int8,
+    OUT shared_blk_read_time float8,
+    OUT shared_blk_write_time float8,
+    OUT local_blk_read_time float8,
+    OUT local_blk_write_time float8,
+    OUT temp_blk_read_time float8,
+    OUT temp_blk_write_time float8,
+    OUT wal_records int8,
+    OUT wal_fpi int8,
+    OUT wal_bytes numeric,
+    OUT wal_buffers_full int8,
+    OUT jit_functions int8,
+    OUT jit_generation_time float8,
+    OUT jit_inlining_count int8,
+    OUT jit_inlining_time float8,
+    OUT jit_optimization_count int8,
+    OUT jit_optimization_time float8,
+    OUT jit_emission_count int8,
+    OUT jit_emission_time float8,
+    OUT jit_deform_count int8,
+    OUT jit_deform_time float8,
+    OUT parallel_workers_to_launch int8,
+    OUT parallel_workers_launched int8,
+    OUT generic_plan_calls int8,
+    OUT custom_plan_calls int8,
+    OUT stats_since timestamp with time zone,
+    OUT minmax_stats_since timestamp with time zone
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13'
+LANGUAGE C STRICT VOLATILE PARALLEL SAFE;
+
+CREATE VIEW pg_stat_statements AS
+  SELECT * FROM pg_stat_statements(true);
+
+GRANT SELECT ON pg_stat_statements TO PUBLIC;
diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index e7857f81ec0..887e87c2ed4 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -69,6 +69,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/timestamp.h"
 
 PG_MODULE_MAGIC_EXT(
@@ -114,6 +115,7 @@ typedef enum pgssVersion
 	PGSS_V1_10,
 	PGSS_V1_11,
 	PGSS_V1_12,
+	PGSS_V1_13,
 } pgssVersion;
 
 typedef enum pgssStoreKind
@@ -210,6 +212,8 @@ typedef struct Counters
 											 * to be launched */
 	int64		parallel_workers_launched;	/* # of parallel workers actually
 											 * launched */
+	int64		generic_plan_calls; /* number of calls using a generic plan */
+	int64		custom_plan_calls;	/* number of calls using a custom plan */
 } Counters;
 
 /*
@@ -323,6 +327,7 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_10);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_11);
 PG_FUNCTION_INFO_V1(pg_stat_statements_1_12);
+PG_FUNCTION_INFO_V1(pg_stat_statements_1_13);
 PG_FUNCTION_INFO_V1(pg_stat_statements);
 PG_FUNCTION_INFO_V1(pg_stat_statements_info);
 
@@ -355,7 +360,8 @@ static void pgss_store(const char *query, int64 queryId,
 					   const struct JitInstrumentation *jitusage,
 					   JumbleState *jstate,
 					   int parallel_workers_to_launch,
-					   int parallel_workers_launched);
+					   int parallel_workers_launched,
+					   CachedPlanMode plan_cache_mode);
 static void pg_stat_statements_internal(FunctionCallInfo fcinfo,
 										pgssVersion api_version,
 										bool showtext);
@@ -877,7 +883,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
 				   NULL,
 				   jstate,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_MODE_NOT_SET);
 }
 
 /*
@@ -957,7 +964,8 @@ pgss_planner(Query *parse,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_MODE_NOT_SET);
 	}
 	else
 	{
@@ -1091,7 +1099,8 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
 				   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,
 				   NULL,
 				   queryDesc->estate->es_parallel_workers_to_launch,
-				   queryDesc->estate->es_parallel_workers_launched);
+				   queryDesc->estate->es_parallel_workers_launched,
+				   queryDesc->cached_plan_mode);
 	}
 
 	if (prev_ExecutorEnd)
@@ -1224,7 +1233,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
 				   NULL,
 				   NULL,
 				   0,
-				   0);
+				   0,
+				   PLAN_CACHE_MODE_NOT_SET);
 	}
 	else
 	{
@@ -1287,7 +1297,8 @@ pgss_store(const char *query, int64 queryId,
 		   const struct JitInstrumentation *jitusage,
 		   JumbleState *jstate,
 		   int parallel_workers_to_launch,
-		   int parallel_workers_launched)
+		   int parallel_workers_launched,
+		   CachedPlanMode plan_cache_mode)
 {
 	pgssHashKey key;
 	pgssEntry  *entry;
@@ -1495,6 +1506,11 @@ pgss_store(const char *query, int64 queryId,
 		entry->counters.parallel_workers_to_launch += parallel_workers_to_launch;
 		entry->counters.parallel_workers_launched += parallel_workers_launched;
 
+		if (plan_cache_mode == PLAN_CACHE_MODE_GENERIC)
+			entry->counters.generic_plan_calls++;
+		else if (plan_cache_mode == PLAN_CACHE_MODE_CUSTOM)
+			entry->counters.custom_plan_calls++;
+
 		SpinLockRelease(&entry->mutex);
 	}
 
@@ -1562,7 +1578,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
 #define PG_STAT_STATEMENTS_COLS_V1_10	43
 #define PG_STAT_STATEMENTS_COLS_V1_11	49
 #define PG_STAT_STATEMENTS_COLS_V1_12	52
-#define PG_STAT_STATEMENTS_COLS			52	/* maximum of above */
+#define PG_STAT_STATEMENTS_COLS_V1_13	54
+#define PG_STAT_STATEMENTS_COLS			54	/* maximum of above */
 
 /*
  * Retrieve statement statistics.
@@ -1574,6 +1591,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS)
  * expected API version is identified by embedding it in the C name of the
  * function.  Unfortunately we weren't bright enough to do that for 1.1.
  */
+Datum
+pg_stat_statements_1_13(PG_FUNCTION_ARGS)
+{
+	bool		showtext = PG_GETARG_BOOL(0);
+
+	pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext);
+
+	return (Datum) 0;
+}
+
 Datum
 pg_stat_statements_1_12(PG_FUNCTION_ARGS)
 {
@@ -1732,6 +1759,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			if (api_version != PGSS_V1_12)
 				elog(ERROR, "incorrect number of output arguments");
 			break;
+		case PG_STAT_STATEMENTS_COLS_V1_13:
+			if (api_version != PGSS_V1_13)
+				elog(ERROR, "incorrect number of output arguments");
+			break;
 		default:
 			elog(ERROR, "incorrect number of output arguments");
 	}
@@ -1984,6 +2015,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);
 			values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);
 		}
+		if (api_version >= PGSS_V1_13)
+		{
+			values[i++] = Int64GetDatumFast(tmp.generic_plan_calls);
+			values[i++] = Int64GetDatumFast(tmp.custom_plan_calls);
+		}
 		if (api_version >= PGSS_V1_11)
 		{
 			values[i++] = TimestampTzGetDatum(stats_since);
@@ -1999,6 +2035,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo,
 					 api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 :
 					 api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 :
 					 api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 :
+					 api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 :
 					 -1 /* fail if you forget to update this assert */ ));
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control
index d45ebc12e36..2eee0ceffa8 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.control
+++ b/contrib/pg_stat_statements/pg_stat_statements.control
@@ -1,5 +1,5 @@
 # pg_stat_statements extension
 comment = 'track planning and execution statistics of all SQL statements executed'
-default_version = '1.12'
+default_version = '1.13'
 module_pathname = '$libdir/pg_stat_statements'
 relocatable = true
diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql
new file mode 100644
index 00000000000..f3878889ea6
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/plancache.sql
@@ -0,0 +1,129 @@
+--
+-- Information related to plan cache
+--
+
+--
+-- Setup
+--
+CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$
+DECLARE
+    ret INT;
+BEGIN
+    SELECT $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$
+DECLARE
+    ret INT;
+BEGIN
+    select $1 INTO ret;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- plan cache counters for prepared statements
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+DEALLOCATE p1;
+
+--
+-- plan cache counters for extended query protocol
+--
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+SELECT $1 \parse p1
+SET plan_cache_mode TO auto;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_generic_plan;
+\bind_named p1 1
+;
+SET plan_cache_mode TO force_custom_plan;
+\bind_named p1 1
+;
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+\close_prepared p1
+
+--
+-- plan cache counters for explain|explain (analyze) with prepared statements
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+PREPARE p1 AS SELECT $1;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE p1(1);
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+RESET pg_stat_statements.track;
+DEALLOCATE p1;
+
+--
+-- plan cache counters for functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- plan cache counters for explain|explain (analyze) with functions and procedures
+--
+SET pg_stat_statements.track = 'all';
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+-- plan cache auto
+SET plan_cache_mode TO auto;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force generic plan
+SET plan_cache_mode TO force_generic_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+-- force custom plan
+SET plan_cache_mode TO force_custom_plan;
+EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1);
+EXPLAIN (COSTS OFF) SELECT select_one_func(1);
+CALL select_one_proc(1);
+SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements
+    ORDER BY query COLLATE "C";
+
+--
+-- Cleanup
+--
+DROP FUNCTION select_one_func(int);
+DROP PROCEDURE select_one_proc(int);
diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml
index 7baa07dcdbf..08b5fec9e10 100644
--- a/doc/src/sgml/pgstatstatements.sgml
+++ b/doc/src/sgml/pgstatstatements.sgml
@@ -554,6 +554,24 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>generic_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a generic plan
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>custom_plan_calls</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times the statement was executed using a custom plan
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_since</structfield> <type>timestamp with time zone</type>
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 67b94b91cae..224d273b2b9 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -838,7 +838,7 @@ BeginCopyTo(ParseState *pstate,
 		cstate->queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
 											GetActiveSnapshot(),
 											InvalidSnapshot,
-											dest, NULL, NULL, 0);
+											dest, NULL, NULL, 0, NULL);
 
 		/*
 		 * Call ExecutorStart to prepare the plan for execution.
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..40ba1183207 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -336,7 +336,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* Create a QueryDesc, redirecting output to our tuple receiver */
 		queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext,
 									GetActiveSnapshot(), InvalidSnapshot,
-									dest, params, queryEnv, 0);
+									dest, params, queryEnv, 0, NULL);
 
 		/* call ExecutorStart to prepare the plan for execution */
 		ExecutorStart(queryDesc, GetIntoRelEFlags(into));
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7e2792ead71..1b0b7b64c1b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -371,7 +371,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	/* run it (if needed) and produce output */
 	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
 				   &planduration, (es->buffers ? &bufusage : NULL),
-				   es->memory ? &mem_counters : NULL);
+				   es->memory ? &mem_counters : NULL, NULL);
 }
 
 /*
@@ -495,7 +495,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 			   const char *queryString, ParamListInfo params,
 			   QueryEnvironment *queryEnv, const instr_time *planduration,
 			   const BufferUsage *bufusage,
-			   const MemoryContextCounters *mem_counters)
+			   const MemoryContextCounters *mem_counters,
+			   CachedPlan *cplan)
 {
 	DestReceiver *dest;
 	QueryDesc  *queryDesc;
@@ -549,7 +550,7 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	/* Create a QueryDesc for the query */
 	queryDesc = CreateQueryDesc(plannedstmt, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
-								dest, params, queryEnv, instrument_option);
+								dest, params, queryEnv, instrument_option, cplan);
 
 	/* Select execution options */
 	if (es->analyze)
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index e6f9ab6dfd6..b1394b10b0c 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -995,7 +995,7 @@ execute_sql_string(const char *sql, const char *filename)
 				qdesc = CreateQueryDesc(stmt,
 										sql,
 										GetActiveSnapshot(), NULL,
-										dest, NULL, NULL, 0);
+										dest, NULL, NULL, 0, NULL);
 
 				ExecutorStart(qdesc, 0);
 				ExecutorRun(qdesc, ForwardScanDirection, 0);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 188e26f0e6e..a415209fc85 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -440,7 +440,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
 	/* Create a QueryDesc, redirecting output to our tuple receiver */
 	queryDesc = CreateQueryDesc(plan, queryString,
 								GetActiveSnapshot(), InvalidSnapshot,
-								dest, NULL, NULL, 0);
+								dest, NULL, NULL, 0, NULL);
 
 	/* call ExecutorStart to prepare the plan for execution */
 	ExecutorStart(queryDesc, 0);
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..55e08b2a63f 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -659,7 +659,7 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es,
 		if (pstmt->commandType != CMD_UTILITY)
 			ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv,
 						   &planduration, (es->buffers ? &bufusage : NULL),
-						   es->memory ? &mem_counters : NULL);
+						   es->memory ? &mem_counters : NULL, cplan);
 		else
 			ExplainOneUtility(pstmt->utilityStmt, into, es, pstate, paramLI);
 
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index f3e77bda279..2acb8e738ac 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -1282,7 +1282,7 @@ ExecParallelGetQueryDesc(shm_toc *toc, DestReceiver *receiver,
 	return CreateQueryDesc(pstmt,
 						   queryString,
 						   GetActiveSnapshot(), InvalidSnapshot,
-						   receiver, paramLI, NULL, instrument_options);
+						   receiver, paramLI, NULL, instrument_options, NULL);
 }
 
 /*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 359aafea681..041d44e77b4 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1345,7 +1345,8 @@ postquel_start(execution_state *es, SQLFunctionCachePtr fcache)
 							 dest,
 							 fcache->paramLI,
 							 es->qd ? es->qd->queryEnv : NULL,
-							 0);
+							 0,
+							 fcache->cplan);
 
 	/* Utility commands don't need Executor. */
 	if (es->qd->operation != CMD_UTILITY)
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index ecb2e4ccaa1..afc9e094594 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2695,7 +2695,9 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 										dest,
 										options->params,
 										_SPI_current->queryEnv,
-										0);
+										0,
+										cplan);
+
 				res = _SPI_pquery(qdesc, fire_triggers,
 								  canSetTag ? options->tcount : 0);
 				FreeQueryDesc(qdesc);
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 08791b8f75e..d70f34b5061 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -26,6 +26,7 @@
 #include "tcop/pquery.h"
 #include "tcop/utility.h"
 #include "utils/memutils.h"
+#include "utils/plancache.h"
 #include "utils/snapmgr.h"
 
 
@@ -41,7 +42,8 @@ static void ProcessQuery(PlannedStmt *plan,
 						 ParamListInfo params,
 						 QueryEnvironment *queryEnv,
 						 DestReceiver *dest,
-						 QueryCompletion *qc);
+						 QueryCompletion *qc,
+						 CachedPlan *cplan);
 static void FillPortalStore(Portal portal, bool isTopLevel);
 static uint64 RunFromStore(Portal portal, ScanDirection direction, uint64 count,
 						   DestReceiver *dest);
@@ -72,7 +74,8 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 				DestReceiver *dest,
 				ParamListInfo params,
 				QueryEnvironment *queryEnv,
-				int instrument_options)
+				int instrument_options,
+				CachedPlan *cplan)
 {
 	QueryDesc  *qd = (QueryDesc *) palloc(sizeof(QueryDesc));
 
@@ -93,6 +96,9 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	qd->planstate = NULL;
 	qd->totaltime = NULL;
 
+	/* set cached_plan_mode, if applicable */
+	qd->cached_plan_mode = cplan ? cplan->mode : PLAN_CACHE_MODE_NOT_SET;
+
 	/* not yet executed */
 	qd->already_executed = false;
 
@@ -139,7 +145,8 @@ ProcessQuery(PlannedStmt *plan,
 			 ParamListInfo params,
 			 QueryEnvironment *queryEnv,
 			 DestReceiver *dest,
-			 QueryCompletion *qc)
+			 QueryCompletion *qc,
+			 CachedPlan *cplan)
 {
 	QueryDesc  *queryDesc;
 
@@ -148,7 +155,7 @@ ProcessQuery(PlannedStmt *plan,
 	 */
 	queryDesc = CreateQueryDesc(plan, sourceText,
 								GetActiveSnapshot(), InvalidSnapshot,
-								dest, params, queryEnv, 0);
+								dest, params, queryEnv, 0, cplan);
 
 	/*
 	 * Call ExecutorStart to prepare the plan for execution
@@ -500,7 +507,8 @@ PortalStart(Portal portal, ParamListInfo params,
 											None_Receiver,
 											params,
 											portal->queryEnv,
-											0);
+											0,
+											portal->cplan);
 
 				/*
 				 * If it's a scrollable cursor, executor needs to support
@@ -1273,7 +1281,7 @@ PortalRunMulti(Portal portal,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
-							 dest, qc);
+							 dest, qc, portal->cplan);
 			}
 			else
 			{
@@ -1282,7 +1290,7 @@ PortalRunMulti(Portal portal,
 							 portal->sourceText,
 							 portal->portalParams,
 							 portal->queryEnv,
-							 altdest, NULL);
+							 altdest, NULL, portal->cplan);
 			}
 
 			if (log_executor_stats)
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 89a1c79e984..af7f9932fab 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1140,6 +1140,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->mode = PLAN_CACHE_MODE_NOT_SET;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
@@ -1358,10 +1359,13 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 		plansource->total_custom_cost += cached_plan_cost(plan, true);
 
 		plansource->num_custom_plans++;
+
+		plan->mode = PLAN_CACHE_MODE_CUSTOM;
 	}
 	else
 	{
 		plansource->num_generic_plans++;
+		plan->mode = PLAN_CACHE_MODE_GENERIC;
 	}
 
 	Assert(plan != NULL);
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 3b122f79ed8..bf8e5f5d90d 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -15,6 +15,7 @@
 
 #include "executor/executor.h"
 #include "parser/parse_node.h"
+#include "utils/plancache.h"
 
 struct ExplainState;			/* defined in explain_state.h */
 
@@ -68,7 +69,8 @@ extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into,
 						   ParamListInfo params, QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
 						   const BufferUsage *bufusage,
-						   const MemoryContextCounters *mem_counters);
+						   const MemoryContextCounters *mem_counters,
+						   CachedPlan *cplan);
 
 extern void ExplainPrintPlan(struct ExplainState *es, QueryDesc *queryDesc);
 extern void ExplainPrintTriggers(struct ExplainState *es,
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index 86db3dc8d0d..bdffb2184e7 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -17,6 +17,7 @@
 
 #include "nodes/execnodes.h"
 #include "tcop/dest.h"
+#include "utils/plancache.h"
 
 
 /* ----------------
@@ -51,6 +52,9 @@ typedef struct QueryDesc
 	/* This field is set by ExecutePlan */
 	bool		already_executed;	/* true if previously executed */
 
+	CachedPlanMode cached_plan_mode;	/* the plan cache mode of the cached
+										 * plan, if there is one */
+
 	/* This is always set NULL by the core system, but plugins can change it */
 	struct Instrumentation *totaltime;	/* total time spent in ExecutorRun */
 } QueryDesc;
@@ -63,7 +67,8 @@ extern QueryDesc *CreateQueryDesc(PlannedStmt *plannedstmt,
 								  DestReceiver *dest,
 								  ParamListInfo params,
 								  QueryEnvironment *queryEnv,
-								  int instrument_options);
+								  int instrument_options,
+								  CachedPlan *cplan);
 
 extern void FreeQueryDesc(QueryDesc *qdesc);
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 1baa6d50bfd..434d6406853 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -35,6 +35,14 @@ typedef enum
 	PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN,
 }			PlanCacheMode;
 
+/* possible values for a CachedPlan mode */
+typedef enum
+{
+	PLAN_CACHE_MODE_NOT_SET = 0,
+	PLAN_CACHE_MODE_GENERIC,
+	PLAN_CACHE_MODE_CUSTOM,
+}			CachedPlanMode;
+
 /* GUC parameter */
 extern PGDLLIMPORT int plan_cache_mode;
 
@@ -169,6 +177,7 @@ typedef struct CachedPlan
 								 * changes from this value */
 	int			generation;		/* parent's generation number for this plan */
 	int			refcount;		/* count of live references to this struct */
+	CachedPlanMode mode;		/* The plan cache mode of this cached plan */
 	MemoryContext context;		/* context containing this CachedPlan */
 } CachedPlan;
 
-- 
2.39.5 (Apple Git-154)

