agora inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v5] Avoid orphaned objects dependencies
508+ messages / 2 participants
[nested] [flat]

* [PATCH v5] Avoid orphaned objects dependencies
@ 2024-03-29 15:43  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Bertrand Drouvot @ 2024-03-29 15:43 UTC (permalink / raw)

It's currently possible to create orphaned objects dependencies, for example:

Scenario 1:

session 1: begin; drop schema schem;
session 2: create a function in the schema schem
session 1: commit;

With the above, the function created in session 2 would be linked to a non
existing schema.

Scenario 2:

session 1: begin; create a function in the schema schem
session 2: drop schema schem;
session 1: commit;

With the above, the function created in session 1 would be linked to a non
existing schema.

To avoid those scenarios, a new lock (that conflicts with a lock taken by DROP)
has been put in place when the dependencies are being recorded. With this in
place, the drop schema in scenario 2 would be locked.

Also, after the new lock attempt, the patch checks that the object still exists:
with this in place session 2 in scenario 1 would be locked and would report an
error once session 1 committs (that would not be the case should session 1 abort
the transaction).

If the object is dropped before the new lock attempt is triggered then the patch
would also report an error (but with less details).

The patch adds a few tests for some dependency cases (that would currently produce
orphaned objects):

- schema and function (as the above scenarios)
- alter a dependency (function and schema)
- function and arg type
- function and return type
- function and function
- domain and domain
- table and type
- server and foreign data wrapper
---
 src/backend/catalog/dependency.c              |  54 ++++++++
 src/backend/catalog/objectaddress.c           |  70 ++++++++++
 src/backend/catalog/pg_depend.c               |  12 ++
 src/include/catalog/dependency.h              |   1 +
 src/include/catalog/objectaddress.h           |   1 +
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 .../test_dependencies_locks/.gitignore        |   3 +
 .../modules/test_dependencies_locks/Makefile  |  14 ++
 .../expected/test_dependencies_locks.out      | 129 ++++++++++++++++++
 .../test_dependencies_locks/meson.build       |  12 ++
 .../specs/test_dependencies_locks.spec        |  89 ++++++++++++
 12 files changed, 387 insertions(+)
  22.9% src/backend/catalog/
  43.7% src/test/modules/test_dependencies_locks/expected/
  27.2% src/test/modules/test_dependencies_locks/specs/
   4.5% src/test/modules/test_dependencies_locks/

diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index d4b5b2ade1..a49357bbe2 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -1517,6 +1517,60 @@ AcquireDeletionLock(const ObjectAddress *object, int flags)
 	}
 }
 
+/*
+ * depLockAndCheckObject
+ *
+ * Lock the object that we are about to record a dependency on.
+ * After it's locked, verify that it hasn't been dropped while we
+ * weren't looking.  If the object has been dropped, this function
+ * does not return!
+ */
+void
+depLockAndCheckObject(const ObjectAddress *object)
+{
+	char	   *object_description;
+
+	/*
+	 * Those don't rely on LockDatabaseObject() when being dropped (see
+	 * AcquireDeletionLock()). Also it looks like they can not produce
+	 * orphaned dependent objects when being dropped.
+	 */
+	if (object->classId == RelationRelationId || object->classId == AuthMemRelationId)
+		return;
+
+	object_description = getObjectDescription(object, true);
+
+	/* assume we should lock the whole object not a sub-object */
+	LockDatabaseObject(object->classId, object->objectId, 0, AccessShareLock);
+
+	/* check if object still exists */
+	if (!ObjectByIdExist(object, false))
+	{
+		/*
+		 * It might be possible that we are creating it (for example creating
+		 * a composite type while creating a relation), so bypass the syscache
+		 * lookup and use a dirty snaphot instead to cover this scenario.
+		 */
+		if (!ObjectByIdExist(object, true))
+		{
+			/*
+			 * If the object has been dropped before we get a chance to get
+			 * its description, then emit a generic error message. That looks
+			 * like a good compromise over extra complexity.
+			 */
+			if (object_description)
+				ereport(ERROR, errmsg("%s does not exist", object_description));
+			else
+				ereport(ERROR, errmsg("a dependent object does not exist"));
+		}
+	}
+
+	if (object_description)
+		pfree(object_description);
+
+	return;
+}
+
 /*
  * ReleaseDeletionLock - release an object deletion lock
  *
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 7b536ac6fd..c2b873dd81 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2590,6 +2590,76 @@ get_object_namespace(const ObjectAddress *address)
 	return oid;
 }
 
+/*
+ * ObjectByIdExist
+ *
+ * Return whether the given object exists.
+ *
+ * Works for most catalogs, if no special processing is needed.
+ */
+bool
+ObjectByIdExist(const ObjectAddress *address, bool use_dirty_snapshot)
+{
+	HeapTuple	tuple;
+	int			cache = -1;
+	const ObjectPropertyType *property;
+
+	if (!use_dirty_snapshot)
+	{
+		property = get_object_property_data(address->classId);
+
+		cache = property->oid_catcache_id;
+	}
+
+	if (cache >= 0)
+	{
+		/* Fetch tuple from syscache. */
+		tuple = SearchSysCache1(cache, ObjectIdGetDatum(address->objectId));
+
+		if (!HeapTupleIsValid(tuple))
+		{
+			return false;
+		}
+
+		ReleaseSysCache(tuple);
+
+		return true;
+	}
+	else
+	{
+		Relation	rel;
+		ScanKeyData skey[1];
+		SysScanDesc scan;
+		SnapshotData DirtySnapshot;
+		Snapshot	snapshot;
+
+		if (use_dirty_snapshot)
+		{
+			InitDirtySnapshot(DirtySnapshot);
+			snapshot = &DirtySnapshot;
+		}
+		else
+			snapshot = NULL;
+
+		rel = table_open(address->classId, AccessShareLock);
+
+		ScanKeyInit(&skey[0],
+					get_object_attnum_oid(address->classId),
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(address->objectId));
+
+		scan = systable_beginscan(rel, get_object_oid_index(address->classId), true,
+								  snapshot, 1, skey);
+
+		/* we expect exactly one match */
+		tuple = systable_getnext(scan);
+		systable_endscan(scan);
+		table_close(rel, AccessShareLock);
+
+		return (HeapTupleIsValid(tuple));
+	}
+}
+
 /*
  * Return ObjectType for the given object type as given by
  * getObjectTypeDescription; if no valid ObjectType code exists, but it's a
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5366f7820c..f16af28429 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -108,6 +108,12 @@ recordMultipleDependencies(const ObjectAddress *depender,
 		if (isObjectPinned(referenced))
 			continue;
 
+		/*
+		 * Acquire a lock and check object still exists while recording the
+		 * dependency. XXX - Should we do so only for DEPENDENCY_NORMAL?
+		 */
+		depLockAndCheckObject(referenced);
+
 		if (slot_init_count < max_slots)
 		{
 			slot[slot_stored_count] = MakeSingleTupleTableSlot(RelationGetDescr(dependDesc),
@@ -506,6 +512,12 @@ changeDependencyFor(Oid classId, Oid objectId,
 		return 1;
 	}
 
+	/*
+	 * Acquire a lock and check object still exists while changing the
+	 * dependency.
+	 */
+	depLockAndCheckObject(&objAddr);
+
 	depRel = table_open(DependRelationId, RowExclusiveLock);
 
 	/* There should be existing dependency record(s), so search. */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 7eee66f810..5619b6f55e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -101,6 +101,7 @@ typedef struct ObjectAddresses ObjectAddresses;
 /* in dependency.c */
 
 extern void AcquireDeletionLock(const ObjectAddress *object, int flags);
+extern void depLockAndCheckObject(const ObjectAddress *object);
 
 extern void ReleaseDeletionLock(const ObjectAddress *object);
 
diff --git a/src/include/catalog/objectaddress.h b/src/include/catalog/objectaddress.h
index 3a70d80e32..04891abcc1 100644
--- a/src/include/catalog/objectaddress.h
+++ b/src/include/catalog/objectaddress.h
@@ -53,6 +53,7 @@ extern void check_object_ownership(Oid roleid,
 								   Node *object, Relation relation);
 
 extern Oid	get_object_namespace(const ObjectAddress *address);
+extern bool ObjectByIdExist(const ObjectAddress *address, bool use_dirty_snapshot);
 
 extern bool is_objectclass_supported(Oid class_id);
 extern const char *get_object_class_descr(Oid class_id);
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 256799f520..75f357100f 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -17,6 +17,7 @@ SUBDIRS = \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
 		  test_ddl_deparse \
+		  test_dependencies_locks \
 		  test_dsa \
 		  test_dsm_registry \
 		  test_extensions \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index d8fe059d23..60305dcccd 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -16,6 +16,7 @@ subdir('test_bloomfilter')
 subdir('test_copy_callbacks')
 subdir('test_custom_rmgrs')
 subdir('test_ddl_deparse')
+subdir('test_dependencies_locks')
 subdir('test_dsa')
 subdir('test_dsm_registry')
 subdir('test_extensions')
diff --git a/src/test/modules/test_dependencies_locks/.gitignore b/src/test/modules/test_dependencies_locks/.gitignore
new file mode 100644
index 0000000000..bf000faac4
--- /dev/null
+++ b/src/test/modules/test_dependencies_locks/.gitignore
@@ -0,0 +1,3 @@
+# Generated subdirectories
+/log/
+/output_iso
diff --git a/src/test/modules/test_dependencies_locks/Makefile b/src/test/modules/test_dependencies_locks/Makefile
new file mode 100644
index 0000000000..7491048380
--- /dev/null
+++ b/src/test/modules/test_dependencies_locks/Makefile
@@ -0,0 +1,14 @@
+# src/test/modules/test_dependencies_locks/Makefile
+
+ISOLATION = test_dependencies_locks
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_dependencies_locks
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_dependencies_locks/expected/test_dependencies_locks.out b/src/test/modules/test_dependencies_locks/expected/test_dependencies_locks.out
new file mode 100644
index 0000000000..9b645d7aa5
--- /dev/null
+++ b/src/test/modules/test_dependencies_locks/expected/test_dependencies_locks.out
@@ -0,0 +1,129 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_create_function_in_schema s2_drop_schema s1_commit
+step s1_begin: BEGIN;
+step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+step s2_drop_schema: DROP SCHEMA testschema; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_schema: <... completed>
+ERROR:  cannot drop schema testschema because other objects depend on it
+
+starting permutation: s2_begin s2_drop_schema s1_create_function_in_schema s2_commit
+step s2_begin: BEGIN;
+step s2_drop_schema: DROP SCHEMA testschema;
+step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_function_in_schema: <... completed>
+ERROR:  schema testschema does not exist
+
+starting permutation: s1_begin s1_alter_function_schema s2_drop_alterschema s1_commit
+step s1_begin: BEGIN;
+step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema;
+step s2_drop_alterschema: DROP SCHEMA alterschema; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_alterschema: <... completed>
+ERROR:  cannot drop schema alterschema because other objects depend on it
+
+starting permutation: s2_begin s2_drop_alterschema s1_alter_function_schema s2_commit
+step s2_begin: BEGIN;
+step s2_drop_alterschema: DROP SCHEMA alterschema;
+step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema; <waiting ...>
+step s2_commit: COMMIT;
+step s1_alter_function_schema: <... completed>
+ERROR:  schema alterschema does not exist
+
+starting permutation: s1_begin s1_create_function_with_argtype s2_drop_foo_type s1_commit
+step s1_begin: BEGIN;
+step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql;
+step s2_drop_foo_type: DROP TYPE public.foo; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_foo_type: <... completed>
+ERROR:  cannot drop type foo because other objects depend on it
+
+starting permutation: s2_begin s2_drop_foo_type s1_create_function_with_argtype s2_commit
+step s2_begin: BEGIN;
+step s2_drop_foo_type: DROP TYPE public.foo;
+step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_function_with_argtype: <... completed>
+ERROR:  type foo does not exist
+
+starting permutation: s1_begin s1_create_function_with_rettype s2_drop_foo_rettype s1_commit
+step s1_begin: BEGIN;
+step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1;
+step s2_drop_foo_rettype: DROP DOMAIN id; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_foo_rettype: <... completed>
+ERROR:  cannot drop type id because other objects depend on it
+
+starting permutation: s2_begin s2_drop_foo_rettype s1_create_function_with_rettype s2_commit
+step s2_begin: BEGIN;
+step s2_drop_foo_rettype: DROP DOMAIN id;
+step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_function_with_rettype: <... completed>
+ERROR:  type id does not exist
+
+starting permutation: s1_begin s1_create_function_with_function s2_drop_function_f s1_commit
+step s1_begin: BEGIN;
+step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1;
+step s2_drop_function_f: DROP FUNCTION f(); <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_function_f: <... completed>
+ERROR:  cannot drop function f() because other objects depend on it
+
+starting permutation: s2_begin s2_drop_function_f s1_create_function_with_function s2_commit
+step s2_begin: BEGIN;
+step s2_drop_function_f: DROP FUNCTION f();
+step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_function_with_function: <... completed>
+ERROR:  function f() does not exist
+
+starting permutation: s1_begin s1_create_domain_with_domain s2_drop_domain_id s1_commit
+step s1_begin: BEGIN;
+step s1_create_domain_with_domain: CREATE DOMAIN idid as id;
+step s2_drop_domain_id: DROP DOMAIN id; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_domain_id: <... completed>
+ERROR:  cannot drop type id because other objects depend on it
+
+starting permutation: s2_begin s2_drop_domain_id s1_create_domain_with_domain s2_commit
+step s2_begin: BEGIN;
+step s2_drop_domain_id: DROP DOMAIN id;
+step s1_create_domain_with_domain: CREATE DOMAIN idid as id; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_domain_with_domain: <... completed>
+ERROR:  type id does not exist
+
+starting permutation: s1_begin s1_create_table_with_type s2_drop_footab_type s1_commit
+step s1_begin: BEGIN;
+step s1_create_table_with_type: CREATE TABLE tabtype(a footab);
+step s2_drop_footab_type: DROP TYPE public.footab; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_footab_type: <... completed>
+ERROR:  cannot drop type footab because other objects depend on it
+
+starting permutation: s2_begin s2_drop_footab_type s1_create_table_with_type s2_commit
+step s2_begin: BEGIN;
+step s2_drop_footab_type: DROP TYPE public.footab;
+step s1_create_table_with_type: CREATE TABLE tabtype(a footab); <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_table_with_type: <... completed>
+ERROR:  type footab does not exist
+
+starting permutation: s1_begin s1_create_server_with_fdw_wrapper s2_drop_fdw_wrapper s1_commit
+step s1_begin: BEGIN;
+step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper;
+step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_fdw_wrapper: <... completed>
+ERROR:  cannot drop foreign-data wrapper fdw_wrapper because other objects depend on it
+
+starting permutation: s2_begin s2_drop_fdw_wrapper s1_create_server_with_fdw_wrapper s2_commit
+step s2_begin: BEGIN;
+step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT;
+step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_server_with_fdw_wrapper: <... completed>
+ERROR:  foreign-data wrapper fdw_wrapper does not exist
diff --git a/src/test/modules/test_dependencies_locks/meson.build b/src/test/modules/test_dependencies_locks/meson.build
new file mode 100644
index 0000000000..92a978ab93
--- /dev/null
+++ b/src/test/modules/test_dependencies_locks/meson.build
@@ -0,0 +1,12 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+tests += {
+  'name': 'test_dependencies_locks',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'isolation': {
+    'specs': [
+      'test_dependencies_locks',
+    ],
+  },
+}
diff --git a/src/test/modules/test_dependencies_locks/specs/test_dependencies_locks.spec b/src/test/modules/test_dependencies_locks/specs/test_dependencies_locks.spec
new file mode 100644
index 0000000000..5d04dfe9dc
--- /dev/null
+++ b/src/test/modules/test_dependencies_locks/specs/test_dependencies_locks.spec
@@ -0,0 +1,89 @@
+setup
+{
+  CREATE SCHEMA testschema;
+  CREATE SCHEMA alterschema;
+  CREATE TYPE public.foo as enum ('one', 'two');
+  CREATE TYPE public.footab as enum ('three', 'four');
+  CREATE DOMAIN id AS int;
+  CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1;
+  CREATE FUNCTION public.falter() RETURNS int LANGUAGE SQL RETURN 1;
+  CREATE FOREIGN DATA WRAPPER fdw_wrapper;
+}
+
+teardown
+{
+  DROP FUNCTION IF EXISTS testschema.foo();
+  DROP FUNCTION IF EXISTS fooargtype(num foo);
+  DROP FUNCTION IF EXISTS footrettype();
+  DROP FUNCTION IF EXISTS foofunc();
+  DROP FUNCTION IF EXISTS public.falter();
+  DROP FUNCTION IF EXISTS alterschema.falter();
+  DROP DOMAIN IF EXISTS idid;
+  DROP SERVER IF EXISTS srv_fdw_wrapper;
+  DROP TABLE IF EXISTS tabtype;
+  DROP SCHEMA IF EXISTS testschema;
+  DROP SCHEMA IF EXISTS alterschema;
+  DROP TYPE IF EXISTS public.foo;
+  DROP TYPE IF EXISTS public.footab;
+  DROP DOMAIN IF EXISTS id;
+  DROP FUNCTION IF EXISTS f();
+  DROP FOREIGN DATA WRAPPER IF EXISTS fdw_wrapper;
+}
+
+session "s1"
+
+step "s1_begin" { BEGIN; }
+step "s1_create_function_in_schema" { CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; }
+step "s1_create_function_with_argtype" { CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; }
+step "s1_create_function_with_rettype" { CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; }
+step "s1_create_function_with_function" { CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; }
+step "s1_alter_function_schema" { ALTER FUNCTION public.falter() SET SCHEMA alterschema; }
+step "s1_create_domain_with_domain" { CREATE DOMAIN idid as id; }
+step "s1_create_table_with_type" { CREATE TABLE tabtype(a footab); }
+step "s1_create_server_with_fdw_wrapper" { CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; }
+step "s1_commit" { COMMIT; }
+
+session "s2"
+
+step "s2_begin" { BEGIN; }
+step "s2_drop_schema" { DROP SCHEMA testschema; }
+step "s2_drop_alterschema" { DROP SCHEMA alterschema; }
+step "s2_drop_foo_type" { DROP TYPE public.foo; }
+step "s2_drop_foo_rettype" { DROP DOMAIN id; }
+step "s2_drop_footab_type" { DROP TYPE public.footab; }
+step "s2_drop_function_f" { DROP FUNCTION f(); }
+step "s2_drop_domain_id" { DROP DOMAIN id; }
+step "s2_drop_fdw_wrapper" { DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; }
+step "s2_commit" { COMMIT; }
+
+# function - schema
+permutation "s1_begin" "s1_create_function_in_schema" "s2_drop_schema" "s1_commit"
+permutation "s2_begin" "s2_drop_schema" "s1_create_function_in_schema" "s2_commit"
+
+# alter function - schema
+permutation "s1_begin" "s1_alter_function_schema" "s2_drop_alterschema" "s1_commit"
+permutation "s2_begin" "s2_drop_alterschema" "s1_alter_function_schema" "s2_commit"
+
+# function - argtype
+permutation "s1_begin" "s1_create_function_with_argtype" "s2_drop_foo_type" "s1_commit"
+permutation "s2_begin" "s2_drop_foo_type" "s1_create_function_with_argtype" "s2_commit"
+
+# function - rettype
+permutation "s1_begin" "s1_create_function_with_rettype" "s2_drop_foo_rettype" "s1_commit"
+permutation "s2_begin" "s2_drop_foo_rettype" "s1_create_function_with_rettype" "s2_commit"
+
+# function - function
+permutation "s1_begin" "s1_create_function_with_function" "s2_drop_function_f" "s1_commit"
+permutation "s2_begin" "s2_drop_function_f" "s1_create_function_with_function" "s2_commit"
+
+# domain - domain
+permutation "s1_begin" "s1_create_domain_with_domain" "s2_drop_domain_id" "s1_commit"
+permutation "s2_begin" "s2_drop_domain_id" "s1_create_domain_with_domain" "s2_commit"
+
+# table - type
+permutation "s1_begin" "s1_create_table_with_type" "s2_drop_footab_type" "s1_commit"
+permutation "s2_begin" "s2_drop_footab_type" "s1_create_table_with_type" "s2_commit"
+
+# server - foreign data wrapper
+permutation "s1_begin" "s1_create_server_with_fdw_wrapper" "s2_drop_fdw_wrapper" "s1_commit"
+permutation "s2_begin" "s2_drop_fdw_wrapper" "s1_create_server_with_fdw_wrapper" "s2_commit"
-- 
2.34.1


--fbv/uQrEQtVlJXri--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v2] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  2 +-
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 27 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index fe0478ac552..9a6bda90070 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -21,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 plancache cleanup oldextversions squashing
+	parallel plancache cleanup oldextversions squashing prepare
 # 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/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 079a8eb5afc..65258651bd2 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -59,6 +59,7 @@ tests += {
       'cleanup',
       'oldextversions',
       'squashing',
+      'prepare',
     ],
     'regress_args': ['--temp-config', files('pg_stat_statements.conf')],
     # Disabled because these tests require
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--soawQPnav6xcFtRL--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v3] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl    |  8 +--
 contrib/pg_stat_statements/Makefile           |  1 +
 .../pg_stat_statements/expected/prepare.out   | 53 +++++++++++++++++++
 contrib/pg_stat_statements/meson.build        |  1 +
 contrib/pg_stat_statements/sql/prepare.sql    | 15 ++++++
 src/backend/commands/prepare.c                | 45 ++++++++++++++--
 src/test/regress/expected/prepare.out         | 44 +++++++++------
 src/test/regress/sql/prepare.sql              |  2 +-
 8 files changed, 143 insertions(+), 26 deletions(-)
 create mode 100644 contrib/pg_stat_statements/expected/prepare.out
 create mode 100644 contrib/pg_stat_statements/sql/prepare.sql

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 5f673bd14c1..f2d8625f8bb 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile
index c27e9529bb6..ac967687796 100644
--- a/contrib/pg_stat_statements/Makefile
+++ b/contrib/pg_stat_statements/Makefile
@@ -36,6 +36,7 @@ REGRESS = \
 	parallel \
 	plancache \
 	squashing \
+	prepare \
 	cleanup \
 	oldextversions
 
diff --git a/contrib/pg_stat_statements/expected/prepare.out b/contrib/pg_stat_statements/expected/prepare.out
new file mode 100644
index 00000000000..010e289c1b0
--- /dev/null
+++ b/contrib/pg_stat_statements/expected/prepare.out
@@ -0,0 +1,53 @@
+-- Tests for PREPARE
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? | ?column? 
+----------+----------
+        1 |        1
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | SELECT $1
+     1 |    1 | SELECT $1, $2
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+ t 
+---
+ t
+(1 row)
+
+EXECUTE p1;
+ ?column? 
+----------
+        1
+(1 row)
+
+EXECUTE p2(0);
+ ?column? 
+----------
+        0
+(1 row)
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+ calls | rows |                       query                        
+-------+------+----------------------------------------------------
+     1 |    1 | PREPARE p1 AS SELECT 1
+     1 |    1 | PREPARE p2(int) AS SELECT 2 * $1
+     1 |    1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
+(3 rows)
+
diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build
index 9d78cb88b7d..c9e1d446d39 100644
--- a/contrib/pg_stat_statements/meson.build
+++ b/contrib/pg_stat_statements/meson.build
@@ -58,6 +58,7 @@ tests += {
       'parallel',
       'plancache',
       'squashing',
+      'prepare',
       'cleanup',
       'oldextversions',
     ],
diff --git a/contrib/pg_stat_statements/sql/prepare.sql b/contrib/pg_stat_statements/sql/prepare.sql
new file mode 100644
index 00000000000..a6bc5de4430
--- /dev/null
+++ b/contrib/pg_stat_statements/sql/prepare.sql
@@ -0,0 +1,15 @@
+-- Tests for PREPARE
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+-- Test that prepared statements in a multi-query string behaves as expected
+SELECT 1\;PREPARE p1 AS SELECT 1\; PREPARE p2(int) AS SELECT 2 * $1\; SELECT 1, 1;
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
+
+SELECT pg_stat_statements_reset() IS NOT NULL AS t;
+
+EXECUTE p1;
+EXECUTE p2(0);
+
+SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C";
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5b86a727587..b8fe3dd5302 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
@@ -116,7 +153,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
-	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
+	query_list = pg_analyze_and_rewrite_varparams(rawstmt, new_query,
 												  &argtypes, &nargs, NULL);
 
 	/* Finish filling in the CachedPlanSource */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--xBRJzsblsXcilRYX--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread

* [PATCH v1] Cleanup explicit PREPARE query strings
@ 2025-12-24 14:31  Julien Rouhaud <[email protected]>
  0 siblings, 0 replies; 508+ messages in thread

From: Julien Rouhaud @ 2025-12-24 14:31 UTC (permalink / raw)

When a multi statements query string contains one PREPARE statement (or
multiple), the whole query string was saved in the cached plan.  This is
wasteful as that string can be artitrarily big, but it can also confusing as
some other parts like pg_prepared_statements will output the saved query string
as-is.

This commit changes this behavior and only stores the part of the query string
that correspond to any given PREPARED statement, similarly to how it's already
done in pg_stat_statements.
---
 contrib/auto_explain/t/001_auto_explain.pl |  8 ++--
 src/backend/commands/prepare.c             | 43 +++++++++++++++++++--
 src/test/regress/expected/prepare.out      | 44 +++++++++++++---------
 src/test/regress/sql/prepare.sql           |  2 +-
 4 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl
index 6af5ac1da18..535c4770095 100644
--- a/contrib/auto_explain/t/001_auto_explain.pl
+++ b/contrib/auto_explain/t/001_auto_explain.pl
@@ -60,7 +60,7 @@ $log_contents = query_log($node,
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1;/,
+	qr/Query Text: PREPARE get_proc\(name\) AS SELECT \* FROM pg_proc WHERE proname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -82,7 +82,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 like(
@@ -98,7 +98,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1;/,
+	qr/Query Text: PREPARE get_type\(name\) AS SELECT \* FROM pg_type WHERE typname = \$1/,
 	"prepared query text logged, text mode");
 
 unlike(
@@ -164,7 +164,7 @@ $log_contents = query_log(
 
 like(
 	$log_contents,
-	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1;"/,
+	qr/"Query Text": "PREPARE get_class\(name\) AS SELECT \* FROM pg_class WHERE relname = \$1"/,
 	"prepared query text logged, json mode");
 
 like(
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 34b6410d6a2..11b330ab73d 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -27,6 +27,7 @@
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
@@ -64,6 +65,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	Oid		   *argtypes = NULL;
 	int			nargs;
 	List	   *query_list;
+	const char *new_query;
 
 	/*
 	 * Disallow empty-string statement name (conflicts with protocol-level
@@ -80,14 +82,49 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 */
 	rawstmt = makeNode(RawStmt);
 	rawstmt->stmt = stmt->query;
-	rawstmt->stmt_location = stmt_location;
-	rawstmt->stmt_len = stmt_len;
+
+	/*
+	 * Extract the query text if possible.
+	 *
+	 * If we have a statement location, we can extract the relevant part of the
+	 * possibly multi-statement query string.  If not just use what we were
+	 * given.
+	 */
+	if (stmt_location < 0)
+	{
+		rawstmt->stmt_location = stmt_location;
+		rawstmt->stmt_len = stmt_len;
+		new_query = pstate->p_sourcetext;
+	}
+	else
+	{
+		const char *cleaned;
+		char	   *tmp;
+
+		rawstmt->stmt_len = stmt_len;
+		cleaned = CleanQuerytext(pstate->p_sourcetext, &stmt_location,
+								 &rawstmt->stmt_len);
+
+		if (rawstmt->stmt_len == 0)
+			rawstmt->stmt_len = strlen(cleaned);
+
+		/*
+		 * CleanQuerytext() removes any leading whitespace and returns a
+		 * pointer to the first actual character, so the cleaned query string
+		 * is guaranteed to start at offset 0.
+		 */
+		rawstmt->stmt_location = 0;
+		tmp = palloc(rawstmt->stmt_len + 1);
+		strlcpy(tmp, cleaned, rawstmt->stmt_len + 1);
+
+		new_query = tmp;
+	}
 
 	/*
 	 * Create the CachedPlanSource before we do parse analysis, since it needs
 	 * to see the unmodified raw parse tree.
 	 */
-	plansource = CreateCachedPlan(rawstmt, pstate->p_sourcetext,
+	plansource = CreateCachedPlan(rawstmt, new_query,
 								  CreateCommandTag(stmt->query));
 
 	/* Transform list of TypeNames to array of type OIDs */
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39c..c645a4e5d0e 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -6,7 +6,17 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+-----------+-----------------+--------------
 (0 rows)
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
+ ?column? 
+----------
+ bingo
+(1 row)
+
+ ?column? 
+----------
+       42
+(1 row)
+
 EXECUTE q1;
  a 
 ---
@@ -14,9 +24,9 @@ EXECUTE q1;
 (1 row)
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 1 AS a; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 1 AS a | {}              | {integer}
 (1 row)
 
 -- should fail
@@ -33,18 +43,18 @@ EXECUTE q1;
 
 PREPARE q2 AS SELECT 2 AS b;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q1   | PREPARE q1 AS SELECT 2;      | {}              | {integer}
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q1   | PREPARE q1 AS SELECT 2      | {}              | {integer}
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (2 rows)
 
 -- sql92 syntax
 DEALLOCATE PREPARE q1;
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
- name |          statement           | parameter_types | result_types 
-------+------------------------------+-----------------+--------------
- q2   | PREPARE q2 AS SELECT 2 AS b; | {}              | {integer}
+ name |          statement          | parameter_types | result_types 
+------+-----------------------------+-----------------+--------------
+ q2   | PREPARE q2 AS SELECT 2 AS b | {}              | {integer}
 (1 row)
 
 DEALLOCATE PREPARE q2;
@@ -168,20 +178,20 @@ SELECT name, statement, parameter_types, result_types FROM pg_prepared_statement
 ------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
  q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
       |         SELECT datname, datistemplate, datallowconn             +|                                                    | 
-      |         FROM pg_database WHERE datname = $1;                     |                                                    | 
+      |         FROM pg_database WHERE datname = $1                      |                                                    | 
  q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    | 
       |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
       |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
+      |         ORDER BY unique1                                         |                                                    | 
  q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2     |                                                    | 
  q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
+      |     SELECT * FROM road WHERE thepath = $1                        |                                                    | 
  q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1            |                                                    | 
 (6 rows)
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95ce..0e7fe44725e 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -4,7 +4,7 @@
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
 
-PREPARE q1 AS SELECT 1 AS a;
+SELECT 'bingo'\;  PREPARE q1 AS SELECT 1 AS a \; SELECT 42;
 EXECUTE q1;
 
 SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements;
-- 
2.52.0


--biRhQYMBrGQlqcCN--





^ permalink  raw  reply  [nested|flat] 508+ messages in thread


end of thread, other threads:[~2025-12-24 14:31 UTC | newest]

Thread overview: 508+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-03-29 15:43 [PATCH v5] Avoid orphaned objects dependencies Bertrand Drouvot <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v3] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v1] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>
2025-12-24 14:31 [PATCH v2] Cleanup explicit PREPARE query strings Julien Rouhaud <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox