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:
@@ -5619,9 +5619,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
|
||||
TupleDesc tupDesc;
|
||||
ForPortionOfExpr *forPortionOf;
|
||||
Datum targetRange;
|
||||
bool isNull;
|
||||
ExprContext *econtext;
|
||||
ExprState *exprState;
|
||||
ForPortionOfState *fpoState;
|
||||
|
||||
rootRelInfo = mtstate->resultRelInfo;
|
||||
@@ -5632,22 +5629,31 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
|
||||
forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
|
||||
|
||||
/* Eval the FOR PORTION OF target */
|
||||
if (mtstate->ps.ps_ExprContext == NULL)
|
||||
ExecAssignExprContext(estate, &mtstate->ps);
|
||||
econtext = mtstate->ps.ps_ExprContext;
|
||||
if (!(eflags & EXEC_FLAG_EXPLAIN_ONLY))
|
||||
{
|
||||
bool isNull;
|
||||
ExprContext *econtext;
|
||||
ExprState *exprState;
|
||||
|
||||
exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate);
|
||||
targetRange = ExecEvalExpr(exprState, econtext, &isNull);
|
||||
if (mtstate->ps.ps_ExprContext == NULL)
|
||||
ExecAssignExprContext(estate, &mtstate->ps);
|
||||
econtext = mtstate->ps.ps_ExprContext;
|
||||
|
||||
/*
|
||||
* FOR PORTION OF ... TO ... FROM should never give us a NULL target,
|
||||
* but FOR PORTION OF (...) could.
|
||||
*/
|
||||
if (isNull)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
|
||||
errmsg("FOR PORTION OF target must not be null"),
|
||||
executor_errposition(estate, forPortionOf->targetLocation)));
|
||||
exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate);
|
||||
targetRange = ExecEvalExpr(exprState, econtext, &isNull);
|
||||
|
||||
/*
|
||||
* FOR PORTION OF ... FROM ... TO should never give us a NULL
|
||||
* target, but FOR PORTION OF (...) could.
|
||||
*/
|
||||
if (isNull)
|
||||
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 */
|
||||
|
||||
|
||||
@@ -2895,4 +2895,93 @@ SELECT definition FROM pg_rules WHERE rulename = 'fpo_param_r';
|
||||
|
||||
DROP TABLE fpo_param_src;
|
||||
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;
|
||||
|
||||
@@ -1920,4 +1920,48 @@ SELECT definition FROM pg_rules WHERE rulename = 'fpo_param_r';
|
||||
DROP TABLE fpo_param_src;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user