v0-0001-Basic-routines-for-testing-saved-SPI-plans.patch

text/plain

Filename: v0-0001-Basic-routines-for-testing-saved-SPI-plans.patch
Type: text/plain
Part: 0
Message: Re: Let plan_cache_mode to be a little less strict

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v0-0001
Subject: Basic routines for testing saved SPI plans
File+
src/test/regress/expected/misc_functions.out 12 0
src/test/regress/regress.c 199 0
src/test/regress/sql/misc_functions.sql 15 0
src/test/regress/sql/plancache.sql 19 0
From 0af47577ed00aae884b2cc11d10b1f058a896c2c Mon Sep 17 00:00:00 2001
From: "Andrei V. Lepikhov" <lepihov@gmail.com>
Date: Fri, 22 Aug 2025 13:39:04 +0200
Subject: [PATCH v0] Basic routines for testing saved SPI plans

---
 src/test/regress/expected/misc_functions.out |  12 ++
 src/test/regress/regress.c                   | 199 +++++++++++++++++++
 src/test/regress/sql/misc_functions.sql      |  15 ++
 src/test/regress/sql/plancache.sql           |  19 ++
 4 files changed, 245 insertions(+)

diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index c3b2b9d8603..0ce8b37a769 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -914,6 +914,18 @@ SELECT test_relpath();
  
 (1 row)
 
+CREATE FUNCTION prepare_spi_plan(text, text, VARIADIC text[])
+RETURNS bigint
+    AS :'regresslib', 'prepare_spi_plan'
+    LANGUAGE C;
+CREATE FUNCTION execute_spi_plan(bigint, text, "any")
+RETURNS SETOF record
+    AS :'regresslib', 'execute_spi_plan'
+    LANGUAGE C STRICT;
+CREATE FUNCTION free_spi_plan(bigint)
+RETURNS void
+    AS :'regresslib', 'free_spi_plan'
+    LANGUAGE C STRICT;
 -- pg_replication_origin.roname limit
 SELECT pg_replication_origin_create('regress_' || repeat('a', 505));
 ERROR:  replication origin name is too long
diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c
index 465ac148ac9..f19d544e601 100644
--- a/src/test/regress/regress.c
+++ b/src/test/regress/regress.c
@@ -42,9 +42,11 @@
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/geo_decls.h"
+#include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/typcache.h"
+#include "utils/varlena.h"
 
 #define EXPECT_TRUE(expr)	\
 	do { \
@@ -1028,3 +1030,200 @@ test_relpath(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+static int
+setup_cursor_options(const char *options_string)
+{
+	List	   *optionLst = NIL;
+	ListCell   *lc;
+	int			cursorOptions = 0;
+
+	if (!SplitIdentifierString(pstrdup(options_string), ',', &optionLst))
+		/* syntax error in option list */
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter \"%s\" must be a list of parameters",
+						"cursor_options")));
+
+	foreach(lc, optionLst)
+	{
+		const char *option = (const char *) lfirst(lc);
+
+		if (pg_strcasecmp(option, "fast_plan") == 0)
+			cursorOptions |= CURSOR_OPT_FAST_PLAN;
+		else if (pg_strcasecmp(option, "generic_plan") == 0)
+			cursorOptions |= CURSOR_OPT_GENERIC_PLAN;
+		else if (pg_strcasecmp(option, "custom_plan") == 0)
+			cursorOptions |= CURSOR_OPT_CUSTOM_PLAN;
+		else if (pg_strcasecmp(option, "parallel_ok") == 0)
+			cursorOptions |= CURSOR_OPT_PARALLEL_OK;
+		else
+			ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("unknown cursor option %s", option)));
+	}
+
+	list_free(optionLst);
+	return cursorOptions;
+}
+
+ /*
+  * Prepare a plan cache entry for incoming parameterised query with specific
+  * set of cursor options.
+  *
+  * cursor_options string may contain any combination of the following values:
+  * 'fast_plan', 'generic_plan', 'custom_plan', or 'parallel_ok'.
+  *
+  * Returns internal pointer to saved SPI plan.
+  */
+PG_FUNCTION_INFO_V1(prepare_spi_plan);
+Datum
+prepare_spi_plan(PG_FUNCTION_ARGS)
+{
+	const char *query_text;
+	int			cursorOptions = 0;
+	ArrayType  *arr;
+	Datum	   *elements = NULL;
+	bool	   *nulls = NULL;
+	Oid			element_type = InvalidOid;
+	int16		elmlen;
+	bool		elmbyval;
+	char		elmalign;
+	int			nitems;
+	int			i;
+	Oid		   *argtypes;
+	SPIPlanPtr	result;
+
+	Assert(PG_NARGS() == 3 && get_fn_expr_variadic(fcinfo->flinfo));
+
+	/* Check supplied arguments */
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("query text cannot be null")));
+	query_text = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	if (PG_ARGISNULL(2))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("list of parameters canot be null")));
+
+	if (!PG_ARGISNULL(1))
+	{
+		char   *options = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+		cursorOptions = setup_cursor_options(options);
+	}
+
+	/* Prepare the Oid array of the query parameters */
+
+	arr = PG_GETARG_ARRAYTYPE_P(2);
+	element_type = ARR_ELEMTYPE(arr);
+	get_typlenbyvalalign(element_type, &elmlen, &elmbyval, &elmalign);
+
+	/* Extract all array elements */
+	deconstruct_array(arr, element_type, elmlen, elmbyval, elmalign,
+					  &elements, &nulls, &nitems);
+
+	argtypes = palloc(nitems * sizeof(Oid));
+	for (i = 0; i < nitems; i++)
+	{
+		const char *typname;
+
+		if (nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("type name cannot be NULL")));
+
+		typname = TextDatumGetCString(elements[i]);
+		argtypes[i] = DirectFunctionCall1Coll(regtypein,
+											  InvalidOid,
+											  CStringGetDatum(typname));
+	}
+
+	/* Create plancache entry and save the plan */
+	SPI_connect();
+	result = SPI_prepare_cursor(query_text, nitems, argtypes, cursorOptions);
+	result = SPI_saveplan(result);
+	SPI_finish();
+	PG_RETURN_POINTER(result);
+}
+
+PG_FUNCTION_INFO_V1(execute_spi_plan);
+Datum
+execute_spi_plan(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo  *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	SPIPlanPtr		plan = (SPIPlanPtr) PG_GETARG_POINTER(0);
+	int				nargs = PG_NARGS();
+	Datum		   *values;
+	char		   *Nulls;
+	int				spirc;
+	int				i;
+
+	if (!SPI_plan_is_valid(plan))
+		PG_RETURN_NULL();
+
+	Assert(!get_fn_expr_variadic(fcinfo->flinfo));
+
+	/*
+	 * Caller wants to change cursor options. Update it for each statement.
+	 */
+	if (!PG_ARGISNULL(1))
+	{
+		char 	   *options = text_to_cstring(PG_GETARG_TEXT_PP(1));
+		List	   *plansources = SPI_plan_get_plan_sources(plan);
+		int			cursorOptions = 0;
+		ListCell   *lc;
+
+		cursorOptions = setup_cursor_options(options);
+
+		foreach(lc, plansources)
+		{
+			CachedPlanSource *src = lfirst(lc);
+
+			src->cursor_options = cursorOptions;
+		}
+	}
+
+	values = palloc((nargs - 2) * sizeof(Datum));
+	Nulls = palloc((nargs - 2) * sizeof(char *));
+	for (i = 2; i < nargs; i++)
+	{
+		if (!PG_ARGISNULL(i))
+		{
+			Nulls[i - 2] = ' ';
+			values[i - 2] = PG_GETARG_DATUM(i);
+		}
+		else
+			Nulls[i - 2] = 'n';
+	}
+
+	SPI_connect();
+	spirc = SPI_execute_plan(plan, values, Nulls, false, 0);
+	if (spirc <= 0)
+		elog(ERROR, "failed to execute the SPI plan %d", spirc);
+
+	rsinfo->expectedDesc = SPI_tuptable->tupdesc;
+	InitMaterializedSRF(fcinfo, MAT_SRF_USE_EXPECTED_DESC | MAT_SRF_BLESS);
+
+	for (i = 0; i < SPI_processed; i++)
+	{
+		tuplestore_puttuple(rsinfo->setResult, SPI_tuptable->vals[i]);
+	}
+
+	SPI_finish();
+
+	return (Datum) 0;
+}
+
+PG_FUNCTION_INFO_V1(free_spi_plan);
+Datum
+free_spi_plan(PG_FUNCTION_ARGS)
+{
+	SPIPlanPtr plan = (SPIPlanPtr) PG_GETARG_POINTER(0);
+
+	SPI_connect();
+	SPI_freeplan(plan);
+	SPI_finish();
+	PG_RETURN_VOID();
+}
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 23792c4132a..8b2c69c51c4 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -412,5 +412,20 @@ CREATE FUNCTION test_relpath()
     LANGUAGE C;
 SELECT test_relpath();
 
+CREATE FUNCTION prepare_spi_plan(text, text, VARIADIC text[])
+RETURNS bigint
+    AS :'regresslib', 'prepare_spi_plan'
+    LANGUAGE C;
+
+CREATE FUNCTION execute_spi_plan(bigint, text, "any")
+RETURNS SETOF record
+    AS :'regresslib', 'execute_spi_plan'
+    LANGUAGE C STRICT;
+
+CREATE FUNCTION free_spi_plan(bigint)
+RETURNS void
+    AS :'regresslib', 'free_spi_plan'
+    LANGUAGE C STRICT;
+
 -- pg_replication_origin.roname limit
 SELECT pg_replication_origin_create('regress_' || repeat('a', 505));
diff --git a/src/test/regress/sql/plancache.sql b/src/test/regress/sql/plancache.sql
index 4b2f11dcc64..7b52a5ac343 100644
--- a/src/test/regress/sql/plancache.sql
+++ b/src/test/regress/sql/plancache.sql
@@ -223,3 +223,22 @@ select name, generic_plans, custom_plans from pg_prepared_statements
   where  name = 'test_mode_pp';
 
 drop table test_mode;
+
+-- Check the interference between plan_cache_mode and cursor_options
+-- EXPLAIN (COSTS OFF, GENERIC_PLAN)
+
+SELECT prepare_spi_plan(NULL, NULL, NULL); -- ERROR
+SELECT prepare_spi_plan(
+  'EXPLAIN (COSTS OFF) SELECT * FROM pcachetest WHERE q1 = $1',
+  NULL, 'integer') AS p1 \gset
+SELECT prepare_spi_plan(
+  'SELECT * FROM pcachetest WHERE q1 = $1 OR q1 = $2',
+  NULL, 'integer', NULL); --ERROR
+SELECT prepare_spi_plan(
+  'SELECT * FROM pcachetest WHERE q1 = $1 OR q1 = 3',
+  NULL, 'integer', 'numeric') AS p2 \gset
+
+SELECT execute_spi_plan(:p1, 'generic_plan', 42);
+
+SELECT free_spi_plan(:p1);
+SELECT free_spi_plan(:p2);
-- 
2.51.0