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;
|
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 */
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user