From 6c8c386693a2568ea5b1f0dd6b7a854c067ca849 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Tue, 7 Apr 2026 12:03:07 +0800
Subject: [PATCH v1 1/1] refactoring transformForPortionOfClause

We should use coerce_to_target_type more, not can_coerce_type, coerce_type
individually.  coerce_to_target_type also handles `UNKNOWN` constant, which
ensures the deparsing casts to the correct data type.

for example:

create view v1 as select 'a';
src9=# \sv v1
CREATE OR REPLACE VIEW public.v1 AS
 SELECT 'a'::text AS "?column?"

context: https://git.postgresql.org/cgit/postgresql.git/commit/?id=8e72d914c52876525a90b28444453de8085c866f
---
 src/backend/parser/analyze.c                 | 140 +++++++++++--------
 src/test/regress/expected/for_portion_of.out |  11 +-
 src/test/regress/sql/for_portion_of.sql      |   3 +
 3 files changed, 89 insertions(+), 65 deletions(-)

diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 84deed9aaa6..6c897af43b5 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -24,6 +24,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/stratnum.h"
 #include "access/sysattr.h"
 #include "catalog/dependency.h"
@@ -1317,6 +1318,7 @@ transformForPortionOfClause(ParseState *pstate,
 	int			range_attno = InvalidAttrNumber;
 	Form_pg_attribute attr;
 	Oid			attbasetype;
+	int32		attbasetypmod = -1;
 	Oid			opclass;
 	Oid			opfamily;
 	Oid			opcintype;
@@ -1346,7 +1348,7 @@ transformForPortionOfClause(ParseState *pstate,
 				 parser_errposition(pstate, forPortionOf->location)));
 	attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
 
-	attbasetype = getBaseType(attr->atttypid);
+	attbasetype = getBaseTypeAndTypmod(attr->atttypid, &attbasetypmod);
 
 	rangeVar = makeVar(rtindex,
 					   range_attno,
@@ -1369,50 +1371,46 @@ transformForPortionOfClause(ParseState *pstate,
 
 	if (forPortionOf->target)
 	{
-		Oid			declared_target_type = attbasetype;
 		Oid			actual_target_type;
 
+		/*
+		 * XXX: For now we only support ranges and multiranges, so we fail on
+		 * anything else.
+		 */
+		if (!type_is_range(attbasetype) && !type_is_multirange(attbasetype))
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					errmsg("column \"%s\" of relation \"%s\" is not a range or multirange type",
+						   forPortionOf->range_name,
+						   RelationGetRelationName(targetrel)),
+					parser_errposition(pstate, forPortionOf->location));
+
 		/*
 		 * We were already given an expression for the target, so we don't
 		 * have to build anything. We still have to make sure we got the right
 		 * type. NULL will be caught be the executor.
 		 */
-
 		result->targetRange = transformExpr(pstate,
 											forPortionOf->target,
 											EXPR_KIND_FOR_PORTION);
 
 		actual_target_type = exprType(result->targetRange);
 
-		if (!can_coerce_type(1, &actual_target_type, &declared_target_type, COERCION_IMPLICIT))
-			ereport(ERROR,
-					(errcode(ERRCODE_DATATYPE_MISMATCH),
-					 errmsg("could not coerce FOR PORTION OF target from %s to %s",
-							format_type_be(actual_target_type),
-							format_type_be(declared_target_type)),
-					 parser_errposition(pstate, exprLocation(forPortionOf->target))));
-
-		result->targetRange = coerce_type(pstate,
-										  result->targetRange,
-										  actual_target_type,
-										  declared_target_type,
-										  -1,
-										  COERCION_IMPLICIT,
-										  COERCE_IMPLICIT_CAST,
-										  exprLocation(forPortionOf->target));
+		result->targetRange =
+			coerce_to_target_type(pstate, result->targetRange, exprType(result->targetRange),
+								  attbasetype,
+								  attbasetypmod,
+								  COERCION_IMPLICIT,
+								  COERCE_IMPLICIT_CAST,
+								  exprLocation(forPortionOf->target));
 
-		/*
-		 * XXX: For now we only support ranges and multiranges, so we fail on
-		 * anything else.
-		 */
-		if (!type_is_range(attbasetype) && !type_is_multirange(attbasetype))
+		if (result->targetRange == NULL)
 			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
-					 errmsg("column \"%s\" of relation \"%s\" is not a range or multirange type",
-							forPortionOf->range_name,
-							RelationGetRelationName(targetrel)),
-					 parser_errposition(pstate, forPortionOf->location)));
-
+					errcode(ERRCODE_DATATYPE_MISMATCH),
+					errmsg("could not coerce FOR PORTION OF target from %s to %s",
+						   format_type_be(actual_target_type),
+						   format_type_be(attbasetype)),
+					parser_errposition(pstate, exprLocation(forPortionOf->target)));
 	}
 	else
 	{
@@ -1420,6 +1418,8 @@ transformForPortionOfClause(ParseState *pstate,
 		Oid			declared_arg_types[2];
 		Oid			actual_arg_types[2];
 		List	   *args;
+		HeapTuple	tup;
+		int32		declared_arg_typTypeMod;
 
 		/*
 		 * Make sure it's a range column. XXX: We could support this syntax on
@@ -1438,6 +1438,12 @@ transformForPortionOfClause(ParseState *pstate,
 		declared_arg_types[0] = rngsubtype;
 		declared_arg_types[1] = rngsubtype;
 
+		tup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rngsubtype));
+		if (!HeapTupleIsValid(tup))
+			elog(ERROR, "cache lookup failed for type %u", rngsubtype);
+		declared_arg_typTypeMod = ((Form_pg_type) GETSTRUCT(tup))->typtypmod;
+		ReleaseSysCache(tup);
+
 		/*
 		 * Build a range from the FROM ... TO ... bounds. This should give a
 		 * constant result, so we accept functions like NOW() but not column
@@ -1451,31 +1457,42 @@ transformForPortionOfClause(ParseState *pstate,
 										 EXPR_KIND_FOR_PORTION);
 		actual_arg_types[0] = exprType(result->targetFrom);
 		actual_arg_types[1] = exprType(result->targetTo);
+
+		result->targetFrom =
+			coerce_to_target_type(pstate, result->targetFrom, actual_arg_types[0],
+								  declared_arg_types[0], declared_arg_typTypeMod,
+								  COERCION_IMPLICIT,
+								  COERCE_IMPLICIT_CAST,
+								  exprLocation(forPortionOf->target_start));
+
+		if (result->targetFrom == NULL)
+			ereport(ERROR,
+					errcode(ERRCODE_DATATYPE_MISMATCH),
+					errmsg("could not coerce FOR PORTION OF %s bound from %s to %s",
+						   "FROM",
+						   format_type_be(actual_arg_types[0]),
+						   format_type_be(declared_arg_types[0])),
+					parser_errposition(pstate, exprLocation(forPortionOf->target_start)));
+
+		result->targetTo =
+			coerce_to_target_type(pstate, result->targetTo, actual_arg_types[1],
+								  declared_arg_types[1], declared_arg_typTypeMod,
+								  COERCION_IMPLICIT,
+								  COERCE_IMPLICIT_CAST,
+								  exprLocation(forPortionOf->target_end));
+
+		if (result->targetTo == NULL)
+			ereport(ERROR,
+					errcode(ERRCODE_DATATYPE_MISMATCH),
+					errmsg("could not coerce FOR PORTION OF %s bound from %s to %s",
+						   "TO",
+						   format_type_be(actual_arg_types[1]),
+						   format_type_be(declared_arg_types[1])),
+					parser_errposition(pstate, exprLocation(forPortionOf->target_end)));
+
 		args = list_make2(copyObject(result->targetFrom),
 						  copyObject(result->targetTo));
 
-		/*
-		 * Check the bound types separately, for better error message and
-		 * location
-		 */
-		if (!can_coerce_type(1, actual_arg_types, declared_arg_types, COERCION_IMPLICIT))
-			ereport(ERROR,
-					(errcode(ERRCODE_DATATYPE_MISMATCH),
-					 errmsg("could not coerce FOR PORTION OF %s bound from %s to %s",
-							"FROM",
-							format_type_be(actual_arg_types[0]),
-							format_type_be(declared_arg_types[0])),
-					 parser_errposition(pstate, exprLocation(forPortionOf->target_start))));
-		if (!can_coerce_type(1, &actual_arg_types[1], &declared_arg_types[1], COERCION_IMPLICIT))
-			ereport(ERROR,
-					(errcode(ERRCODE_DATATYPE_MISMATCH),
-					 errmsg("could not coerce FOR PORTION OF %s bound from %s to %s",
-							"TO",
-							format_type_be(actual_arg_types[1]),
-							format_type_be(declared_arg_types[1])),
-					 parser_errposition(pstate, exprLocation(forPortionOf->target_end))));
-
-		make_fn_arguments(pstate, args, actual_arg_types, declared_arg_types);
 		result->targetRange = (Node *) makeFuncExpr(get_range_constructor2(attbasetype),
 													attbasetype,
 													args,
@@ -1483,7 +1500,8 @@ transformForPortionOfClause(ParseState *pstate,
 	}
 	if (contain_volatile_functions_after_planning((Expr *) result->targetRange))
 		ereport(ERROR,
-				(errmsg("FOR PORTION OF bounds cannot contain volatile functions")));
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("FOR PORTION OF bounds cannot contain volatile functions"));
 
 	/*
 	 * Build overlapsExpr to use as an extra qual. This means we only hit rows
@@ -1575,14 +1593,14 @@ transformForPortionOfClause(ParseState *pstate,
 		 * Coerce to domain if necessary. If we skip this, we will allow
 		 * updating to forbidden values.
 		 */
-		rangeTLEExpr = coerce_type(pstate,
-								   rangeTLEExpr,
-								   attbasetype,
-								   attr->atttypid,
-								   -1,
-								   COERCION_IMPLICIT,
-								   COERCE_IMPLICIT_CAST,
-								   exprLocation(forPortionOf->target));
+		rangeTLEExpr = coerce_to_target_type(pstate,
+											 rangeTLEExpr,
+											 attbasetype,
+											 attr->atttypid,
+											 attr->atttypmod,
+											 COERCION_IMPLICIT,
+											 COERCE_IMPLICIT_CAST,
+											 exprLocation(forPortionOf->target));
 
 		/* Make a TLE to set the range column */
 		result->rangeTargetList = NIL;
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 31f772c723d..747fe3183cb 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -652,7 +652,7 @@ END;
          RETURNS text
          LANGUAGE sql
 1       BEGIN ATOMIC
-2        UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' SET name = 'one^1'::text
+2        UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15'::date TO '2019-01-01'::date SET name = 'one^1'::text
 3          RETURNING for_portion_of_test.name;
 4       END
 CREATE OR REPLACE function fpo_update()
@@ -1010,6 +1010,8 @@ DELETE FROM for_portion_of_test WHERE id IN ('[10,11)');
 CREATE FUNCTION fpo_delete()
 RETURNS text
 BEGIN ATOMIC
+  DELETE FROM for_portion_of_test
+    FOR PORTION OF valid_at ('[2018-01-15, 2019-01-01)');
   DELETE FROM for_portion_of_test
     FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
     RETURNING name;
@@ -1019,9 +1021,10 @@ END;
          RETURNS text
          LANGUAGE sql
 1       BEGIN ATOMIC
-2        DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
-3          RETURNING for_portion_of_test.name;
-4       END
+2        DELETE FROM for_portion_of_test FOR PORTION OF valid_at ('[2018-01-15,2019-01-01)'::daterange);
+3        DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15'::date TO '2019-01-01'::date
+4          RETURNING for_portion_of_test.name;
+5       END
 CREATE OR REPLACE function fpo_delete()
 RETURNS text
 BEGIN ATOMIC
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index d4062acf1d1..938ae036634 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -651,6 +651,9 @@ DELETE FROM for_portion_of_test WHERE id IN ('[10,11)');
 CREATE FUNCTION fpo_delete()
 RETURNS text
 BEGIN ATOMIC
+  DELETE FROM for_portion_of_test
+    FOR PORTION OF valid_at ('[2018-01-15, 2019-01-01)');
+
   DELETE FROM for_portion_of_test
     FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
     RETURNING name;
-- 
2.34.1

