From 6c8c386693a2568ea5b1f0dd6b7a854c067ca849 Mon Sep 17 00:00:00 2001 From: jian he 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