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

MDEV-41211 Federated multi-table DELETE keeps a const table row

A multi-table DELETE on a FEDERATED or FederatedX table went wrong
when a primary key lookup made the target a const table.  The
optimizer reads a const table's row through index_read_idx_map(),
whose default implementation ends the index scan and frees the result
set.  The server asks for the row's position later, during execution,
so the saved position was empty.  FederatedX skipped the row and
FEDERATED crashed in rnd_pos().

Both engines now override index_read_idx_map() so that the lookup
leaves its result set open, as index_read() does.  position() then
records a valid position, and the result set is freed at the end of
the statement.  FEDERATED's reset() now also clears stored_result,
which otherwise pointed at a freed result set and was freed again
when the table was closed.

Tests include a multitable UPDATE with a const target, which crashed
both engines before the fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dave Gosselin
2026-10-02 09:44:41 -04:00
committed by Nikita Malyavin
parent c76793ffde
commit 8cc04fdd9f
6 changed files with 175 additions and 8 deletions

View File

@@ -18,6 +18,59 @@ drop table t1;
select * from fed;
Got one of the listed errors
drop table fed;
#
# MDEV-41211 Federated multi_delete skips some records
#
connection slave;
CREATE TABLE federated.t1 (pk INT NOT NULL PRIMARY KEY, a INT);
INSERT INTO federated.t1 VALUES (1,1),(5,5),(9,9);
connection master;
CREATE TABLE federated.t1 (pk INT NOT NULL PRIMARY KEY, a INT)
ENGINE=FEDERATED
CONNECTION='mysql://root@127.0.0.1:SLAVE_PORT/federated/t1';
EXPLAIN DELETE a FROM federated.t1 a, federated.t1 b WHERE a.pk = 5 AND b.pk > 0;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE a const PRIMARY PRIMARY 4 const 1
1 SIMPLE b range PRIMARY PRIMARY 4 NULL 2 Using where
DELETE a FROM federated.t1 a, federated.t1 b WHERE a.pk = 5 AND b.pk > 0;
SELECT * FROM federated.t1 ORDER BY pk;
pk a
1 1
9 9
# Both tables are const
EXPLAIN DELETE a FROM federated.t1 a, federated.t1 b WHERE a.pk = 1 AND b.pk = 9;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE a const PRIMARY PRIMARY 4 const 1
1 SIMPLE b const PRIMARY PRIMARY 4 const 1
DELETE a FROM federated.t1 a, federated.t1 b WHERE a.pk = 1 AND b.pk = 9;
SELECT * FROM federated.t1 ORDER BY pk;
pk a
9 9
# Multi-table UPDATE with a const target
INSERT INTO federated.t1 VALUES (1,1),(5,5);
EXPLAIN UPDATE federated.t1 a, federated.t1 b SET a.a = 0
WHERE a.pk = 5 AND b.pk > 0;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE a const PRIMARY PRIMARY 4 const 1
1 SIMPLE b range PRIMARY PRIMARY 4 NULL 2 Using where
UPDATE federated.t1 a, federated.t1 b SET a.a = 0 WHERE a.pk = 5 AND b.pk > 0;
SELECT * FROM federated.t1 ORDER BY pk;
pk a
1 1
5 0
9 9
connection slave;
SELECT * FROM federated.t1 ORDER BY pk;
pk a
1 1
5 0
9 9
connection master;
DROP TABLE federated.t1;
connection slave;
DROP TABLE federated.t1;
connection master;
# End of 11.4 tests
connection master;
DROP TABLE IF EXISTS federated.t1;
DROP DATABASE IF EXISTS federated;

View File

@@ -21,5 +21,46 @@ drop table t1;
select * from fed;
drop table fed;
--echo #
--echo # MDEV-41211 Federated multi_delete skips some records
--echo #
connection slave;
CREATE TABLE federated.t1 (pk INT NOT NULL PRIMARY KEY, a INT);
INSERT INTO federated.t1 VALUES (1,1),(5,5),(9,9);
connection master;
--replace_result $SLAVE_MYPORT SLAVE_PORT
eval CREATE TABLE federated.t1 (pk INT NOT NULL PRIMARY KEY, a INT)
ENGINE=FEDERATED
CONNECTION='mysql://root@127.0.0.1:$SLAVE_MYPORT/federated/t1';
EXPLAIN DELETE a FROM federated.t1 a, federated.t1 b WHERE a.pk = 5 AND b.pk > 0;
DELETE a FROM federated.t1 a, federated.t1 b WHERE a.pk = 5 AND b.pk > 0;
SELECT * FROM federated.t1 ORDER BY pk;
--echo # Both tables are const
EXPLAIN DELETE a FROM federated.t1 a, federated.t1 b WHERE a.pk = 1 AND b.pk = 9;
DELETE a FROM federated.t1 a, federated.t1 b WHERE a.pk = 1 AND b.pk = 9;
SELECT * FROM federated.t1 ORDER BY pk;
--echo # Multi-table UPDATE with a const target
INSERT INTO federated.t1 VALUES (1,1),(5,5);
EXPLAIN UPDATE federated.t1 a, federated.t1 b SET a.a = 0
WHERE a.pk = 5 AND b.pk > 0;
UPDATE federated.t1 a, federated.t1 b SET a.a = 0 WHERE a.pk = 5 AND b.pk > 0;
SELECT * FROM federated.t1 ORDER BY pk;
connection slave;
SELECT * FROM federated.t1 ORDER BY pk;
connection master;
DROP TABLE federated.t1;
connection slave;
DROP TABLE federated.t1;
connection master;
--echo # End of 11.4 tests
source include/federated_cleanup.inc;

View File

@@ -2374,14 +2374,44 @@ int ha_federated::delete_row(const uchar *buf)
int ha_federated::index_read(uchar *buf, const uchar *key,
uint key_len, ha_rkey_function find_flag)
{
int rc;
DBUG_ENTER("ha_federated::index_read");
DBUG_RETURN(index_read_keep_result(buf, active_index, key, key_len,
find_flag));
}
/*
The default implementation ends the index scan after the read, which
frees the result set. A const table is read this way during
optimization, but position() is called for its row during execution, so
the result set must stay in stored_result.
*/
int ha_federated::index_read_idx_map(uchar *buf, uint index,
const uchar *key,
key_part_map keypart_map,
enum ha_rkey_function find_flag)
{
uint key_len= calculate_key_len(table, index, key, keypart_map);
DBUG_ENTER("ha_federated::index_read_idx_map");
DBUG_RETURN(index_read_keep_result(buf, index, key, key_len, find_flag));
}
/*
Reads the first row matching key and keeps its result set in
stored_result, so that position() can record the row.
*/
int ha_federated::index_read_keep_result(uchar *buf, uint index,
const uchar *key, uint key_len,
ha_rkey_function find_flag)
{
DBUG_ENTER("ha_federated::index_read_keep_result");
free_result();
rc= index_read_idx_with_result_set(buf, active_index, key,
key_len, find_flag,
&stored_result);
DBUG_RETURN(rc);
DBUG_RETURN(index_read_idx_with_result_set(buf, index, key, key_len,
find_flag, &stored_result));
}
@@ -2968,6 +2998,7 @@ int ha_federated::reset(void)
mysql_free_result(result);
}
reset_dynamic(&results);
stored_result= 0;
if (mysql)
mysql->net.thd= NULL;

View File

@@ -117,6 +117,8 @@ private:
uint key_len,
ha_rkey_function find_flag,
MYSQL_RES **result);
int index_read_keep_result(uchar *buf, uint index, const uchar *key,
uint key_len, ha_rkey_function find_flag);
int real_query(const char *query, size_t length);
int real_connect();
public:
@@ -219,6 +221,9 @@ public:
ha_rows estimate_rows_upper_bound() override;
int index_read(uchar *buf, const uchar *key,
uint key_len, enum ha_rkey_function find_flag) override;
int index_read_idx_map(uchar *buf, uint index, const uchar *key,
key_part_map keypart_map,
enum ha_rkey_function find_flag) override;
int index_next(uchar *buf) override;
int index_end() override;
int read_range_first(const key_range *start_key,

View File

@@ -2585,12 +2585,44 @@ int ha_federatedx::index_read(uchar *buf, const uchar *key,
uint key_len, ha_rkey_function find_flag)
{
DBUG_ENTER("ha_federatedx::index_read");
DBUG_RETURN(index_read_keep_result(buf, active_index, key, key_len,
find_flag));
}
/*
The default implementation ends the index scan after the read, which
frees the result set. A const table is read this way during
optimization, but position() is called for its row during execution, so
the result set must stay in stored_result.
*/
int ha_federatedx::index_read_idx_map(uchar *buf, uint index,
const uchar *key,
key_part_map keypart_map,
enum ha_rkey_function find_flag)
{
uint key_len= calculate_key_len(table, index, key, keypart_map);
DBUG_ENTER("ha_federatedx::index_read_idx_map");
DBUG_RETURN(index_read_keep_result(buf, index, key, key_len, find_flag));
}
/*
Reads the first row matching key and keeps its result set in
stored_result, so that position() can record the row.
*/
int ha_federatedx::index_read_keep_result(uchar *buf, uint index,
const uchar *key, uint key_len,
ha_rkey_function find_flag)
{
DBUG_ENTER("ha_federatedx::index_read_keep_result");
if (stored_result)
(void) free_result();
DBUG_RETURN(index_read_idx_with_result_set(buf, active_index, key,
key_len, find_flag,
&stored_result));
DBUG_RETURN(index_read_idx_with_result_set(buf, index, key, key_len,
find_flag, &stored_result));
}

View File

@@ -311,6 +311,8 @@ private:
uint key_len,
ha_rkey_function find_flag,
FEDERATEDX_IO_RESULT **result);
int index_read_keep_result(uchar *buf, uint index, const uchar *key,
uint key_len, ha_rkey_function find_flag);
int real_query(const char *query, uint length);
int real_connect(FEDERATEDX_SHARE *my_share, uint create_flag);
public:
@@ -405,6 +407,9 @@ public:
ha_rows estimate_rows_upper_bound() override;
int index_read(uchar *buf, const uchar *key,
uint key_len, enum ha_rkey_function find_flag) override;
int index_read_idx_map(uchar *buf, uint index, const uchar *key,
key_part_map keypart_map,
enum ha_rkey_function find_flag) override;
int index_next(uchar *buf) override;
int index_end() override;
int read_range_first(const key_range *start_key,