Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit dddb564

Browse files
committed
Fix incorrect non-strict join recheck in MERGE WHEN NOT MATCHED BY SOURCE.
If a MERGE command contains WHEN NOT MATCHED BY SOURCE actions, the merge join condition is used by the executor to distinguish MATCHED from NOT MATCHED BY SOURCE cases. However, this qual is executed using the output from the join subplan node, which nulls the output from the source relation in the not matched case, and so the result may be incorrect if the join condition is "non-strict" -- for example, something like "src.col IS NOT DISTINCT FROM tgt.col". Fix this by enhancing the join recheck condition with an additional "src IS NOT NULL" check, so that it does the right thing when evaluated using the output from the join subplan. Noted by Tom Lane while investigating bug #18634 from Alexander Lakhin. Back-patch to v17, where WHEN NOT MATCHED BY SOURCE support was added to MERGE. Discussion: https://postgr.es/m/18634-db5299c937877f2b%40postgresql.org
1 parent 1953196 commit dddb564

File tree

3 files changed

+101
-5
lines changed

3 files changed

+101
-5
lines changed

src/backend/optimizer/prep/prepjointree.c

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ transform_MERGE_to_join(Query *parse)
158158
int joinrti;
159159
List *vars;
160160
RangeTblRef *rtr;
161+
FromExpr *target;
162+
Node *source;
163+
int sourcerti;
161164

162165
if (parse->commandType != CMD_MERGE)
163166
return;
@@ -226,13 +229,36 @@ transform_MERGE_to_join(Query *parse)
226229
* parse->jointree->quals are restrictions on the target relation (if the
227230
* target relation is an auto-updatable view).
228231
*/
232+
/* target rel, with any quals */
229233
rtr = makeNode(RangeTblRef);
230234
rtr->rtindex = parse->mergeTargetRelation;
235+
target = makeFromExpr(list_make1(rtr), parse->jointree->quals);
236+
237+
/* source rel (expect exactly one -- see transformMergeStmt()) */
238+
Assert(list_length(parse->jointree->fromlist) == 1);
239+
source = linitial(parse->jointree->fromlist);
240+
241+
/*
242+
* index of source rel (expect either a RangeTblRef or a JoinExpr -- see
243+
* transformFromClauseItem()).
244+
*/
245+
if (IsA(source, RangeTblRef))
246+
sourcerti = ((RangeTblRef *) source)->rtindex;
247+
else if (IsA(source, JoinExpr))
248+
sourcerti = ((JoinExpr *) source)->rtindex;
249+
else
250+
{
251+
elog(ERROR, "unrecognized source node type: %d",
252+
(int) nodeTag(source));
253+
sourcerti = 0; /* keep compiler quiet */
254+
}
255+
256+
/* Join the source and target */
231257
joinexpr = makeNode(JoinExpr);
232258
joinexpr->jointype = jointype;
233259
joinexpr->isNatural = false;
234-
joinexpr->larg = (Node *) makeFromExpr(list_make1(rtr), parse->jointree->quals);
235-
joinexpr->rarg = linitial(parse->jointree->fromlist); /* source rel */
260+
joinexpr->larg = (Node *) target;
261+
joinexpr->rarg = source;
236262
joinexpr->usingClause = NIL;
237263
joinexpr->join_using_alias = NULL;
238264
joinexpr->quals = parse->mergeJoinCondition;
@@ -261,9 +287,39 @@ transform_MERGE_to_join(Query *parse)
261287
* use the join condition to distinguish between MATCHED and NOT MATCHED
262288
* BY SOURCE cases. Otherwise, it's no longer needed, and we set it to
263289
* NULL, saving cycles during planning and execution.
264-
*/
265-
if (!have_action[MERGE_WHEN_NOT_MATCHED_BY_SOURCE])
266-
parse->mergeJoinCondition = NULL;
290+
*
291+
* We need to be careful though: the executor evaluates this condition
292+
* using the output of the join subplan node, which nulls the output from
293+
* the source relation when the join condition doesn't match. That risks
294+
* producing incorrect results when rechecking using a "non-strict" join
295+
* condition, such as "src.col IS NOT DISTINCT FROM tgt.col". To guard
296+
* against that, we add an additional "src IS NOT NULL" check to the join
297+
* condition, so that it does the right thing when performing a recheck
298+
* based on the output of the join subplan.
299+
*/
300+
if (have_action[MERGE_WHEN_NOT_MATCHED_BY_SOURCE])
301+
{
302+
Var *var;
303+
NullTest *ntest;
304+
305+
/* source wholerow Var (nullable by the new join) */
306+
var = makeWholeRowVar(rt_fetch(sourcerti, parse->rtable),
307+
sourcerti, 0, false);
308+
var->varnullingrels = bms_make_singleton(joinrti);
309+
310+
/* "src IS NOT NULL" check */
311+
ntest = makeNode(NullTest);
312+
ntest->arg = (Expr *) var;
313+
ntest->nulltesttype = IS_NOT_NULL;
314+
ntest->argisrow = false;
315+
ntest->location = -1;
316+
317+
/* combine it with the original join condition */
318+
parse->mergeJoinCondition =
319+
(Node *) make_and_qual((Node *) ntest, parse->mergeJoinCondition);
320+
}
321+
else
322+
parse->mergeJoinCondition = NULL; /* join condition not needed */
267323
}
268324

269325
/*

src/test/regress/expected/merge.out

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,6 +2689,28 @@ DETAIL: drop cascades to table measurement_y2006m02
26892689
drop cascades to table measurement_y2006m03
26902690
drop cascades to table measurement_y2007m01
26912691
DROP FUNCTION measurement_insert_trigger();
2692+
--
2693+
-- test non-strict join clause
2694+
--
2695+
CREATE TABLE src (a int, b text);
2696+
INSERT INTO src VALUES (1, 'src row');
2697+
CREATE TABLE tgt (a int, b text);
2698+
INSERT INTO tgt VALUES (NULL, 'tgt row');
2699+
MERGE INTO tgt USING src ON tgt.a IS NOT DISTINCT FROM src.a
2700+
WHEN MATCHED THEN UPDATE SET a = src.a, b = src.b
2701+
WHEN NOT MATCHED BY SOURCE THEN DELETE
2702+
RETURNING merge_action(), src.*, tgt.*;
2703+
merge_action | a | b | a | b
2704+
--------------+---+---+---+---------
2705+
DELETE | | | | tgt row
2706+
(1 row)
2707+
2708+
SELECT * FROM tgt;
2709+
a | b
2710+
---+---
2711+
(0 rows)
2712+
2713+
DROP TABLE src, tgt;
26922714
-- prepare
26932715
RESET SESSION AUTHORIZATION;
26942716
-- try a system catalog

src/test/regress/sql/merge.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,24 @@ SELECT * FROM new_measurement ORDER BY city_id, logdate;
17101710
DROP TABLE measurement, new_measurement CASCADE;
17111711
DROP FUNCTION measurement_insert_trigger();
17121712

1713+
--
1714+
-- test non-strict join clause
1715+
--
1716+
CREATE TABLE src (a int, b text);
1717+
INSERT INTO src VALUES (1, 'src row');
1718+
1719+
CREATE TABLE tgt (a int, b text);
1720+
INSERT INTO tgt VALUES (NULL, 'tgt row');
1721+
1722+
MERGE INTO tgt USING src ON tgt.a IS NOT DISTINCT FROM src.a
1723+
WHEN MATCHED THEN UPDATE SET a = src.a, b = src.b
1724+
WHEN NOT MATCHED BY SOURCE THEN DELETE
1725+
RETURNING merge_action(), src.*, tgt.*;
1726+
1727+
SELECT * FROM tgt;
1728+
1729+
DROP TABLE src, tgt;
1730+
17131731
-- prepare
17141732

17151733
RESET SESSION AUTHORIZATION;

0 commit comments

Comments
 (0)