Hi Hackers, Here is a fix for findings D2 & D12 from [0].
The issue is FOR PORTION OF bounds are not coerced, so PREPARE requires casting them and a NULL deparses as ::unknown not ::date/::timestamp/etc. We actually do coerce these bounds, since we have to build a range constructor, but we don't put those into the ForPortionOfExpr struct. This commit fixes that. [0] https://www.postgresql.org/message-id/CA%2BrenyV6QLOJYmLo3gbsg1Y%2BCrho8NqME1jJXgPbO_NgxfBaKQ%40mail.gmail.com Yours, -- Paul ~{:-) [email protected]
From 5f19a3689a317d29eab274775aa09ceabbf6688a Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" <[email protected]> Date: Thu, 3 Sep 2026 12:18:49 -0700 Subject: [PATCH v1] Resolve untyped parameters in FOR PORTION OF FROM/TO bounds FOR PORTION OF valid_at FROM $1 TO $2 fails with "could not determine data type of parameter $1", so using PREPARE requires users to say $1::date etc. But transformForPortionOfClause() already coerces the bounds, since it builds a constructor for the targeted range. We just need to store the coerced expressions in targetFrom and targetTo. This also improves pg_get_ruledef(), which now renders an untyped NULL bound as NULL::date instead of NULL::unknown. Reported-by: Noah Misch <[email protected]> Author: Paul A. Jungwirth <[email protected]> Backpatch-through: 19 --- src/backend/parser/analyze.c | 12 ++- src/test/regress/expected/for_portion_of.out | 106 ++++++++++++++++++- src/test/regress/sql/for_portion_of.sql | 71 +++++++++++++ 3 files changed, 185 insertions(+), 4 deletions(-) diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 581457c69c9..7a873c8ee7a 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -1462,8 +1462,7 @@ transformForPortionOfClause(ParseState *pstate, EXPR_KIND_FOR_PORTION); actual_arg_types[0] = exprType(result->targetFrom); actual_arg_types[1] = exprType(result->targetTo); - args = list_make2(copyObject(result->targetFrom), - copyObject(result->targetTo)); + args = list_make2(result->targetFrom, result->targetTo); /* * Check the bound types separately, for better error message and @@ -1487,6 +1486,15 @@ transformForPortionOfClause(ParseState *pstate, parser_errposition(pstate, exprLocation(forPortionOf->target_end)))); make_fn_arguments(pstate, args, actual_arg_types, declared_arg_types); + + /* + * Keep the *coerced* bounds. This lets prepared statements use + * parameters without explicit casts, and it improves deparsing when + * FOR PORTION OF appears in a function or RULE. + */ + result->targetFrom = copyObject((Node *) linitial(args)); + result->targetTo = copyObject((Node *) lsecond(args)); + result->targetRange = (Node *) makeFuncExpr(get_range_constructor2(attbasetype), attbasetype, args, diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out index 64789d1777b..e4090a145c2 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() @@ -1029,7 +1029,7 @@ 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' +2 DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15'::date TO '2019-01-01'::date 3 RETURNING for_portion_of_test.name; 4 END CREATE OR REPLACE function fpo_delete() @@ -2793,4 +2793,106 @@ SELECT * FROM fpo_rls ORDER BY valid_at; DROP TABLE fpo_rls; DROP ROLE regress_fpo_rls; +-- +-- Parameters in the FOR PORTION OF bounds +-- +CREATE TABLE fpo_param ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO fpo_param (id, valid_at, name) VALUES + ('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one'); +-- A parameter of unspecified type in an ordinary expression gets its type +-- resolved from context. This is the control case for the FROM/TO bounds +-- below: it builds exactly the same daterange the FROM/TO form does. +PREPARE fpo_param_control AS + UPDATE fpo_param SET name = 'ctl' WHERE valid_at && daterange($1, $2); +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_control'; + parameter_types +----------------- + {date,date} +(1 row) + +-- The (portion) form resolves the parameter type from the range column. +PREPARE fpo_param_portion AS + UPDATE fpo_param FOR PORTION OF valid_at ($1) SET name = 'portion'; +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_portion'; + parameter_types +----------------- + {daterange} +(1 row) + +-- The FROM/TO form should likewise resolve its bounds to the range's +-- subtype, so that clients need not spell out the parameter types. +PREPARE fpo_param_update AS + UPDATE fpo_param FOR PORTION OF valid_at FROM $1 TO $2 SET name = 'upd'; +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_update'; + parameter_types +----------------- + {date,date} +(1 row) + +PREPARE fpo_param_delete AS + DELETE FROM fpo_param FOR PORTION OF valid_at FROM $1 TO $2; +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_delete'; + parameter_types +----------------- + {date,date} +(1 row) + +-- Only one bound parameterized. +PREPARE fpo_param_one AS + UPDATE fpo_param FOR PORTION OF valid_at FROM $1 TO '2003-01-01' + SET name = 'one-bound'; +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_one'; + parameter_types +----------------- + {date} +(1 row) + +-- A parameter used as both bounds must still resolve to one type. +PREPARE fpo_param_same AS + UPDATE fpo_param FOR PORTION OF valid_at FROM $1 TO $1 SET name = 'same'; +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_same'; + parameter_types +----------------- + {date} +(1 row) + +EXECUTE fpo_param_update('2002-01-01', '2003-01-01'); +SELECT * FROM fpo_param ORDER BY valid_at; + id | valid_at | name +-------+-------------------------+------ + [1,2) | [2000-01-01,2002-01-01) | one + [1,2) | [2002-01-01,2003-01-01) | upd + [1,2) | [2003-01-01,2010-01-01) | one +(3 rows) + +DEALLOCATE fpo_param_control; +DEALLOCATE fpo_param_portion; +DEALLOCATE fpo_param_update; +DEALLOCATE fpo_param_delete; +DEALLOCATE fpo_param_one; +DEALLOCATE fpo_param_same; +-- The bounds we keep for deparsing are the coerced ones, so an untyped NULL +-- bound renders with the range's subtype instead of "unknown". +CREATE TABLE fpo_param_src (id int); +CREATE RULE fpo_param_r AS ON DELETE TO fpo_param_src DO INSTEAD + DELETE FROM fpo_param FOR PORTION OF valid_at FROM NULL TO '2001-01-01'; +SELECT definition FROM pg_rules WHERE rulename = 'fpo_param_r'; + definition +---------------------------------------------------------------------------------------------------------------------------------------- + CREATE RULE fpo_param_r AS + + ON DELETE TO public.fpo_param_src DO INSTEAD DELETE FROM fpo_param FOR PORTION OF valid_at FROM NULL::date TO '2001-01-01'::date; +(1 row) + +DROP TABLE fpo_param_src; +DROP TABLE fpo_param; RESET datestyle; diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql index b61fe10478e..ede1f28d0ed 100644 --- a/src/test/regress/sql/for_portion_of.sql +++ b/src/test/regress/sql/for_portion_of.sql @@ -1849,4 +1849,75 @@ SELECT * FROM fpo_rls ORDER BY valid_at; DROP TABLE fpo_rls; DROP ROLE regress_fpo_rls; +-- +-- Parameters in the FOR PORTION OF bounds +-- + +CREATE TABLE fpo_param ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO fpo_param (id, valid_at, name) VALUES + ('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one'); + +-- A parameter of unspecified type in an ordinary expression gets its type +-- resolved from context. This is the control case for the FROM/TO bounds +-- below: it builds exactly the same daterange the FROM/TO form does. +PREPARE fpo_param_control AS + UPDATE fpo_param SET name = 'ctl' WHERE valid_at && daterange($1, $2); +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_control'; + +-- The (portion) form resolves the parameter type from the range column. +PREPARE fpo_param_portion AS + UPDATE fpo_param FOR PORTION OF valid_at ($1) SET name = 'portion'; +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_portion'; + +-- The FROM/TO form should likewise resolve its bounds to the range's +-- subtype, so that clients need not spell out the parameter types. +PREPARE fpo_param_update AS + UPDATE fpo_param FOR PORTION OF valid_at FROM $1 TO $2 SET name = 'upd'; +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_update'; + +PREPARE fpo_param_delete AS + DELETE FROM fpo_param FOR PORTION OF valid_at FROM $1 TO $2; +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_delete'; + +-- Only one bound parameterized. +PREPARE fpo_param_one AS + UPDATE fpo_param FOR PORTION OF valid_at FROM $1 TO '2003-01-01' + SET name = 'one-bound'; +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_one'; + +-- A parameter used as both bounds must still resolve to one type. +PREPARE fpo_param_same AS + UPDATE fpo_param FOR PORTION OF valid_at FROM $1 TO $1 SET name = 'same'; +SELECT parameter_types FROM pg_prepared_statements + WHERE name = 'fpo_param_same'; + +EXECUTE fpo_param_update('2002-01-01', '2003-01-01'); +SELECT * FROM fpo_param ORDER BY valid_at; + +DEALLOCATE fpo_param_control; +DEALLOCATE fpo_param_portion; +DEALLOCATE fpo_param_update; +DEALLOCATE fpo_param_delete; +DEALLOCATE fpo_param_one; +DEALLOCATE fpo_param_same; + +-- The bounds we keep for deparsing are the coerced ones, so an untyped NULL +-- bound renders with the range's subtype instead of "unknown". +CREATE TABLE fpo_param_src (id int); +CREATE RULE fpo_param_r AS ON DELETE TO fpo_param_src DO INSTEAD + DELETE FROM fpo_param FOR PORTION OF valid_at FROM NULL TO '2001-01-01'; +SELECT definition FROM pg_rules WHERE rulename = 'fpo_param_r'; + +DROP TABLE fpo_param_src; +DROP TABLE fpo_param; + RESET datestyle; -- 2.47.3
