1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00

MDEV-40620: Null complement a merged derived table inside a FULL JOIN

A FULL JOIN's null complement pass marks the whole opposite operand as a
null row before it emits a row of the side it rescans.  The walk that
marks the operand treated a TABLE_LIST as either a plain table or a nest.
A merged derived table or view is both, its own placeholder and the
tables it was merged into, so the walk marked the placeholder and
stopped.  The query reads the fields of the merged tables, which still
held the values of an earlier row, and those values reached the client
where NULL belonged.

The walk now descends into the nested tables as well as marking the
placeholder.  Section 22 covers the shape with the flattening of join
nests turned off, which leaves the derived table in a nest of its own so
the walk reaches it.  Before this change the FULL JOIN returned the left
side value 3 against the right side rows 3 and 4, where the UNION of a
LEFT JOIN and a RIGHT JOIN returns NULL.
This commit is contained in:
Dave Gosselin
2026-08-17 11:32:04 -04:00
parent 6af3a27b51
commit 2baedaddfc
3 changed files with 80 additions and 4 deletions

View File

@@ -6199,4 +6199,43 @@ id select_type table type possible_keys key key_len ref rows filtered Extra
Warnings:
Note 1003 select `test`.`t1`.`a` AS `a`,`test`.`t2`.`a` AS `a` from `test`.`t1` full join `test`.`t2` on(`test`.`t2`.`a` is not null and `test`.`t1`.`a` < 3) where 1
drop table t1, t2;
# ====================================================================
# Section 22: A merged derived table inside a FULL JOIN operand
# ====================================================================
#
# The null complement pass marks the whole opposite operand as a null
# row before it emits a row of the side it rescans. A merged derived
# table is both its own placeholder and the tables merged into it, and
# the query reads the fields of those tables, so the walk that marks
# the operand has to reach them. Keeping the nesting the parser built
# leaves the derived table in a nest of its own, which is the shape
# that reaches the walk.
create table t1 (a int, b varchar(10));
insert into t1 values (1,'a'), (2,'b'), (3,'c');
create table t2 (a int, b varchar(10));
insert into t2 values (2,'x'), (3,'y'), (4,'z');
create table t3 (a int, b varchar(10));
insert into t3 values (1,'w'), (2,'v'), (6,'u');
set optimizer_switch='simplify_joins=off';
with vals as (select a, b from t1)
select vals.a as a1, t3.a as a3, t2.a as a2
from (vals join t3 on vals.a = t3.a) full join t2 on vals.a = t2.a;
a1 a3 a2
1 1 NULL
2 2 2
NULL NULL 3
NULL NULL 4
with vals as (select a, b from t1)
select vals.a as a1, t3.a as a3, t2.a as a2
from (vals join t3 on vals.a = t3.a) left join t2 on vals.a = t2.a
union
select vals.a as a1, t3.a as a3, t2.a as a2
from (vals join t3 on vals.a = t3.a) right join t2 on vals.a = t2.a;
a1 a3 a2
1 1 NULL
2 2 2
NULL NULL 3
NULL NULL 4
set optimizer_switch=default;
drop table t1, t2, t3;
# End of 12.3 tests

View File

@@ -3826,4 +3826,39 @@ select t1.a, t2.a from t1 full join t2 on t2.a is not null and t1.a < 3;
drop table t1, t2;
--echo # ====================================================================
--echo # Section 22: A merged derived table inside a FULL JOIN operand
--echo # ====================================================================
--echo #
--echo # The null complement pass marks the whole opposite operand as a null
--echo # row before it emits a row of the side it rescans. A merged derived
--echo # table is both its own placeholder and the tables merged into it, and
--echo # the query reads the fields of those tables, so the walk that marks
--echo # the operand has to reach them. Keeping the nesting the parser built
--echo # leaves the derived table in a nest of its own, which is the shape
--echo # that reaches the walk.
create table t1 (a int, b varchar(10));
insert into t1 values (1,'a'), (2,'b'), (3,'c');
create table t2 (a int, b varchar(10));
insert into t2 values (2,'x'), (3,'y'), (4,'z');
create table t3 (a int, b varchar(10));
insert into t3 values (1,'w'), (2,'v'), (6,'u');
set optimizer_switch='simplify_joins=off';
--sorted_result
with vals as (select a, b from t1)
select vals.a as a1, t3.a as a3, t2.a as a2
from (vals join t3 on vals.a = t3.a) full join t2 on vals.a = t2.a;
--sorted_result
with vals as (select a, b from t1)
select vals.a as a1, t3.a as a3, t2.a as a2
from (vals join t3 on vals.a = t3.a) left join t2 on vals.a = t2.a
union
select vals.a as a1, t3.a as a3, t2.a as a2
from (vals join t3 on vals.a = t3.a) right join t2 on vals.a = t2.a;
set optimizer_switch=default;
drop table t1, t2, t3;
--echo # End of 12.3 tests

View File

@@ -26926,14 +26926,16 @@ sub_select(JOIN *join,JOIN_TAB *join_tab,bool end_of_records)
/*
Recursively mark all base tables within a TABLE_LIST as null rows.
Handles both single tables and nested joins (where table is NULL
but nested_join contains child TABLE_LISTs).
A plain table carries table alone and a nest carries nested_join
alone, but a merged derived table or view carries both, its own
placeholder and the tables it was merged into. The query reads the
fields of those tables, so the walk cannot stop at the placeholder.
*/
static void mark_table_list_as_null_row(TABLE_LIST *tl)
{
if (tl->table)
mark_as_null_row(tl->table);
else if (tl->nested_join)
if (tl->nested_join)
{
List_iterator<TABLE_LIST> li(tl->nested_join->join_list);
TABLE_LIST *child;
@@ -26948,7 +26950,7 @@ static void unmark_table_list_as_null_row(TABLE_LIST *tl)
{
if (tl->table)
unmark_as_null_row(tl->table);
else if (tl->nested_join)
if (tl->nested_join)
{
List_iterator<TABLE_LIST> li(tl->nested_join->join_list);
TABLE_LIST *child;