1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

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 <noah@leadboat.com>
Author: Paul A. Jungwirth <pj@illuminatedcomputing.com>
Discussion: https://www.postgresql.org/message-id/flat/CA%2BrenyW1uJp1dw%3DiK7MsWfbbZf-dKP_4JLoQ65wj%3Dfry35FjCw%40mail.gmail.com
This commit is contained in:
Peter Eisentraut
2026-09-08 09:35:44 +02:00
parent c13e961c60
commit f194032168
3 changed files with 185 additions and 4 deletions

View File

@@ -1465,8 +1465,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
@@ -1490,6 +1489,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,

View File

@@ -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;

View File

@@ -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;