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

Don't evaluate the FOR PORTION OF target under EXPLAIN

ExecInitModifyTable() evaluated ForPortionOfExpr.targetRange
unconditionally at executor start-up, with no EXEC_FLAG_EXPLAIN_ONLY
guard. This caused three problems:

- EXPLAIN (GENERIC_PLAN) failed with "no value found for parameter 1".

- Plain EXPLAIN with FOR PORTION OF (null) raised "FOR PORTION OF
  target must not be null".

- Plain EXPLAIN ran user functions appearing in the target a second
  time, beyond the one evaluation the planner already does for
  selectivity estimation.

Now we skip the evaluation and the accompanying null check in
explain-only mode.

Reported-by: Noah Misch <noah@leadboat.com>
Author: Paul A. Jungwirth <pj@illuminatedcomputing.com>
Discussion: https://www.postgresql.org/message-id/flat/CA%2BrenyXci9khYXJ8uSwzF30DVw%2Bzc4QwGb1RdsuOu_YtrFuy1g%40mail.gmail.com
This commit is contained in:
Peter Eisentraut
2026-09-08 14:34:31 +02:00
parent 25b21c0bb7
commit 140fdfcdf1
3 changed files with 156 additions and 17 deletions

View File

@@ -5619,9 +5619,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
TupleDesc tupDesc; TupleDesc tupDesc;
ForPortionOfExpr *forPortionOf; ForPortionOfExpr *forPortionOf;
Datum targetRange; Datum targetRange;
bool isNull;
ExprContext *econtext;
ExprState *exprState;
ForPortionOfState *fpoState; ForPortionOfState *fpoState;
rootRelInfo = mtstate->resultRelInfo; rootRelInfo = mtstate->resultRelInfo;
@@ -5632,22 +5629,31 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
forPortionOf = (ForPortionOfExpr *) node->forPortionOf; forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
/* Eval the FOR PORTION OF target */ /* Eval the FOR PORTION OF target */
if (mtstate->ps.ps_ExprContext == NULL) if (!(eflags & EXEC_FLAG_EXPLAIN_ONLY))
ExecAssignExprContext(estate, &mtstate->ps); {
econtext = mtstate->ps.ps_ExprContext; bool isNull;
ExprContext *econtext;
ExprState *exprState;
exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate); if (mtstate->ps.ps_ExprContext == NULL)
targetRange = ExecEvalExpr(exprState, econtext, &isNull); ExecAssignExprContext(estate, &mtstate->ps);
econtext = mtstate->ps.ps_ExprContext;
/* exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate);
* FOR PORTION OF ... TO ... FROM should never give us a NULL target, targetRange = ExecEvalExpr(exprState, econtext, &isNull);
* but FOR PORTION OF (...) could.
*/ /*
if (isNull) * FOR PORTION OF ... FROM ... TO should never give us a NULL
ereport(ERROR, * target, but FOR PORTION OF (...) could.
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), */
errmsg("FOR PORTION OF target must not be null"), if (isNull)
executor_errposition(estate, forPortionOf->targetLocation))); ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("FOR PORTION OF target must not be null"),
executor_errposition(estate, forPortionOf->targetLocation)));
}
else
targetRange = (Datum) 0;
/* Create state for FOR PORTION OF operation */ /* Create state for FOR PORTION OF operation */

View File

@@ -2895,4 +2895,93 @@ SELECT definition FROM pg_rules WHERE rulename = 'fpo_param_r';
DROP TABLE fpo_param_src; DROP TABLE fpo_param_src;
DROP TABLE fpo_param; DROP TABLE fpo_param;
--
-- EXPLAIN without ANALYZE must only plan the statement, so it must not
-- evaluate the FOR PORTION OF target.
--
CREATE TABLE fpo_explain (
id int4range,
valid_at daterange,
name text
);
INSERT INTO fpo_explain (id, valid_at, name) VALUES
('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one');
-- GENERIC_PLAN exists precisely so that a statement containing parameter
-- placeholders can be planned without supplying any parameter values. The
-- hand-written equivalent qual is the control case.
EXPLAIN (COSTS OFF, GENERIC_PLAN)
UPDATE fpo_explain SET name = 'q' WHERE valid_at && $1::daterange;
QUERY PLAN
----------------------------------
Update on fpo_explain
-> Seq Scan on fpo_explain
Filter: (valid_at && $1)
(3 rows)
EXPLAIN (COSTS OFF, GENERIC_PLAN)
UPDATE fpo_explain FOR PORTION OF valid_at ($1::daterange) SET name = 'q';
QUERY PLAN
----------------------------------
Update on fpo_explain
-> Seq Scan on fpo_explain
Filter: (valid_at && $1)
(3 rows)
EXPLAIN (COSTS OFF, GENERIC_PLAN)
DELETE FROM fpo_explain FOR PORTION OF valid_at ($1::daterange);
QUERY PLAN
----------------------------------
Delete on fpo_explain
-> Seq Scan on fpo_explain
Filter: (valid_at && $1)
(3 rows)
-- A null target is a run-time error, so plain EXPLAIN should still report
-- the plan.
EXPLAIN (COSTS OFF)
UPDATE fpo_explain FOR PORTION OF valid_at (NULL::daterange) SET name = 'q';
QUERY PLAN
---------------------------------------
Update on fpo_explain
-> Result
Replaces: Scan on fpo_explain
One-Time Filter: false
(4 rows)
EXPLAIN (COSTS OFF)
DELETE FROM fpo_explain FOR PORTION OF valid_at (NULL::daterange);
QUERY PLAN
---------------------------------------
Delete on fpo_explain
-> Result
Replaces: Scan on fpo_explain
One-Time Filter: false
(4 rows)
-- Actually running it is still an error.
UPDATE fpo_explain FOR PORTION OF valid_at (NULL::daterange) SET name = 'q';
ERROR: FOR PORTION OF target must not be null
LINE 1: UPDATE fpo_explain FOR PORTION OF valid_at (NULL::daterange)...
^
DELETE FROM fpo_explain FOR PORTION OF valid_at (NULL::daterange);
ERROR: FOR PORTION OF target must not be null
LINE 1: DELETE FROM fpo_explain FOR PORTION OF valid_at (NULL::dater...
^
-- EXPLAIN ANALYZE does execute, so the null check must still fire there.
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
UPDATE fpo_explain FOR PORTION OF valid_at (NULL::daterange) SET name = 'q';
ERROR: FOR PORTION OF target must not be null
LINE 2: UPDATE fpo_explain FOR PORTION OF valid_at (NULL::daterang...
^
UPDATE fpo_explain FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
SET name = 'q';
SELECT * FROM fpo_explain 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) | q
[1,2) | [2003-01-01,2010-01-01) | one
(3 rows)
DROP TABLE fpo_explain;
RESET datestyle; RESET datestyle;

View File

@@ -1920,4 +1920,48 @@ SELECT definition FROM pg_rules WHERE rulename = 'fpo_param_r';
DROP TABLE fpo_param_src; DROP TABLE fpo_param_src;
DROP TABLE fpo_param; DROP TABLE fpo_param;
--
-- EXPLAIN without ANALYZE must only plan the statement, so it must not
-- evaluate the FOR PORTION OF target.
--
CREATE TABLE fpo_explain (
id int4range,
valid_at daterange,
name text
);
INSERT INTO fpo_explain (id, valid_at, name) VALUES
('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one');
-- GENERIC_PLAN exists precisely so that a statement containing parameter
-- placeholders can be planned without supplying any parameter values. The
-- hand-written equivalent qual is the control case.
EXPLAIN (COSTS OFF, GENERIC_PLAN)
UPDATE fpo_explain SET name = 'q' WHERE valid_at && $1::daterange;
EXPLAIN (COSTS OFF, GENERIC_PLAN)
UPDATE fpo_explain FOR PORTION OF valid_at ($1::daterange) SET name = 'q';
EXPLAIN (COSTS OFF, GENERIC_PLAN)
DELETE FROM fpo_explain FOR PORTION OF valid_at ($1::daterange);
-- A null target is a run-time error, so plain EXPLAIN should still report
-- the plan.
EXPLAIN (COSTS OFF)
UPDATE fpo_explain FOR PORTION OF valid_at (NULL::daterange) SET name = 'q';
EXPLAIN (COSTS OFF)
DELETE FROM fpo_explain FOR PORTION OF valid_at (NULL::daterange);
-- Actually running it is still an error.
UPDATE fpo_explain FOR PORTION OF valid_at (NULL::daterange) SET name = 'q';
DELETE FROM fpo_explain FOR PORTION OF valid_at (NULL::daterange);
-- EXPLAIN ANALYZE does execute, so the null check must still fire there.
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
UPDATE fpo_explain FOR PORTION OF valid_at (NULL::daterange) SET name = 'q';
UPDATE fpo_explain FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
SET name = 'q';
SELECT * FROM fpo_explain ORDER BY valid_at;
DROP TABLE fpo_explain;
RESET datestyle; RESET datestyle;