From 7bf2e663354311cf14e32e572e2552b8783c125b Mon Sep 17 00:00:00 2001 From: Rex Johnston Date: Sat, 6 Dec 2025 07:03:22 +1200 Subject: [PATCH 1/2] MDEV-38258 No error thrown when CTE columns updated in updates set clause CTE's are read only i.e It cannot have their columns updated in updates set clause. Attempting to do so , should throw error ERROR 1288 (HY000): The target table cte of the UPDATE is not updatable with cte as (select * from t1 where c < 5) update cte set cte.a =(select a from cte); Here, conversion from a single table to a multi table update bypasses the normal check by setting the TABLE_LIST object representing cte to be merge derived. This is incorrect, so we prohibit this conversion in TABLE_LIST::init_derived. Approved by: Sanja Byelkin (sanja@mariadb.com) --- mysql-test/main/cte_update_delete.result | 18 ++++++++++++++++++ mysql-test/main/cte_update_delete.test | 22 ++++++++++++++++++++++ sql/table.cc | 3 +++ 3 files changed, 43 insertions(+) diff --git a/mysql-test/main/cte_update_delete.result b/mysql-test/main/cte_update_delete.result index fac0c7e2a68a7..c2cdfc61a0e07 100644 --- a/mysql-test/main/cte_update_delete.result +++ b/mysql-test/main/cte_update_delete.result @@ -1325,5 +1325,23 @@ insert into t3 values (1, 1, 'iron'),(2,2,'wood'),(0,NULL, 'gold'), (3, 3, 'silver'), (4, 4, 'lead'), (5, 5, 'tin'), (6, 6, 'platinum'), (7, 7, 'aluminium'); drop prepare s; +# +# MDEV-38258 No error thrown when CTE columns updated in updates set clause +# +with cte as (select * from t1 where c < 1) +update cte set cte.a =(select a from cte); +ERROR HY000: The target table cte of the UPDATE is not updatable +with cte as (select a from t1) +update cte set cte.a=(select a from cte limit 1); +ERROR HY000: The target table cte of the UPDATE is not updatable +with cte as (select a from t1), +cte2 as (select a from t3) +update t2, cte set cte.a=(select a from cte limit 1); +ERROR HY000: The target table cte of the UPDATE is not updatable +with cte as (select a from t1), +cte2 as (select a from t3) +update cte +set cte.a=(select a from cte where cte.a in (select a from cte2) limit 1); +ERROR HY000: The target table cte of the UPDATE is not updatable drop table t1, t2, t3, t4, t5; # End of 12.2 tests diff --git a/mysql-test/main/cte_update_delete.test b/mysql-test/main/cte_update_delete.test index 2fef3b6722a1e..93666dd24baeb 100644 --- a/mysql-test/main/cte_update_delete.test +++ b/mysql-test/main/cte_update_delete.test @@ -511,6 +511,28 @@ eval $empty_t3; eval $fill_t3; drop prepare s; +--echo # +--echo # MDEV-38258 No error thrown when CTE columns updated in updates set clause +--echo # + +--error ER_NON_UPDATABLE_TABLE +with cte as (select * from t1 where c < 1) + update cte set cte.a =(select a from cte); + +--error ER_NON_UPDATABLE_TABLE +with cte as (select a from t1) + update cte set cte.a=(select a from cte limit 1); + +--error ER_NON_UPDATABLE_TABLE +with cte as (select a from t1), + cte2 as (select a from t3) + update t2, cte set cte.a=(select a from cte limit 1); + +--error ER_NON_UPDATABLE_TABLE +with cte as (select a from t1), + cte2 as (select a from t3) + update cte + set cte.a=(select a from cte where cte.a in (select a from cte2) limit 1); drop table t1, t2, t3, t4, t5; diff --git a/sql/table.cc b/sql/table.cc index 1860ef6d5aa34..2d5f9f30586a8 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -10154,6 +10154,9 @@ bool TABLE_LIST::init_derived(THD *thd, bool init_view) belong_to_view ? belong_to_view->updating : !unit->outer_select()->outer_select(); + if (with && updating) + set_materialized_derived(); + /* In the case where a table merge operation moves a derived table from one select to another, table hints may be adjusted already. From 56c22c455585e124a0ce610b4560627f7bfc18d4 Mon Sep 17 00:00:00 2001 From: Rex Johnston Date: Tue, 9 Dec 2025 02:40:52 +1200 Subject: [PATCH 2/2] MDEV-38272 Sig11 in LEX::resolve_references_to_cte at sql/sql_cte.cc Lex::save_list contents remain from a previous query that has invalid syntax and isn't reset when processing a new query. We initialize this structure along with it's peer in LEX::start. MDEV-38295 Recursive CTE usage leaks memory when not used in a top level select During st_select_lex::cleanup() we assume that leaf tables and any associated recursive table references can only be present when a join structure is present. After MDEV-37220 this is no longer true. We shift our cleanup routine out of the test for a join. We add Multidelete_prelocking_strategy::handle_end() in the same manner as Multiupdate_prelocking_strategy::handle_end() (but with minimal processing as yet) to populate these leaf_tables in the outermost select when it is a delete statement. We add in a call to free_underlaid_joins() to the error processing section of Sql_cmd_dml::execute to free up temporary tables that were allocated in prepare, but not freed up when execute is bypassed because an error occured during prepare. Sergei Petrunia: Better comments for MDEV-38272, MDEV-38272 Approved by: Sergei Petrunia (sergey@mariadb.com) --- mysql-test/main/cte_update_delete.result | 31 +++++++++++++ mysql-test/main/cte_update_delete.test | 59 ++++++++++++++++++++++++ sql/sql_base.h | 8 ++++ sql/sql_delete.cc | 44 ++++++++++++++++++ sql/sql_delete.h | 4 +- sql/sql_lex.cc | 1 + sql/sql_select.cc | 2 + sql/sql_union.cc | 13 +++++- sql/table.cc | 7 +++ 9 files changed, 165 insertions(+), 4 deletions(-) diff --git a/mysql-test/main/cte_update_delete.result b/mysql-test/main/cte_update_delete.result index c2cdfc61a0e07..6cda90fb9e2cd 100644 --- a/mysql-test/main/cte_update_delete.result +++ b/mysql-test/main/cte_update_delete.result @@ -1344,4 +1344,35 @@ update cte set cte.a=(select a from cte where cte.a in (select a from cte2) limit 1); ERROR HY000: The target table cte of the UPDATE is not updatable drop table t1, t2, t3, t4, t5; +# +# MDEV-38272 Sig11 in LEX::resolve_references_to_cte at sql/sql_cte.cc +# +create table t1(a int); +insert into t1 values (1), (5), (10), (15), (20), (25); +create view v1 as select * from t1 where a < 10; +with cte as (select * from t1) delete from t1,cte where t1.a=cte.a; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'where t1.a=cte.a' at line 1 +with cte as (select * from t1) delete from t1 using t1,cte where t1.a=cte.a; +# +# MDEV-38272 Recursive CTE usage leaks memory when not used in a top level select +# +with recursive cte as (select a from t1 union select a from cte) +update t1, cte set t1.o = 1; +ERROR 42S22: Unknown column 't1.o' in 'SET' +with recursive cte as (select a from t1 union select a from cte where a < 2) +delete from cte where a = (select a from cte); +ERROR HY000: The target table cte of the DELETE is not updatable +with recursive cte as (select a from t1 union select a from cte where a < 2) +delete from cte where a = 10; +ERROR HY000: The target table cte of the DELETE is not updatable +insert into t1 values (1), (5), (10), (15), (20), (25); +with recursive cte as (select a from t1 union select a from cte where a < 2) +delete from v1 using v1, cte where v1.a = cte.a; +select * from t1 order by 1; +a +10 +15 +20 +25 +drop table t1; # End of 12.2 tests diff --git a/mysql-test/main/cte_update_delete.test b/mysql-test/main/cte_update_delete.test index 93666dd24baeb..8ec964153c489 100644 --- a/mysql-test/main/cte_update_delete.test +++ b/mysql-test/main/cte_update_delete.test @@ -1,3 +1,4 @@ +--source include/not_embedded.inc --echo # --echo # MDEV-37220 Allow UPDATE/DELETE to read from a CTE --echo # @@ -536,4 +537,62 @@ with cte as (select a from t1), drop table t1, t2, t3, t4, t5; +--echo # +--echo # MDEV-38272 Sig11 in LEX::resolve_references_to_cte at sql/sql_cte.cc +--echo # + +create table t1(a int); +insert into t1 values (1), (5), (10), (15), (20), (25); +create view v1 as select * from t1 where a < 10; +--error ER_PARSE_ERROR +with cte as (select * from t1) delete from t1,cte where t1.a=cte.a; +with cte as (select * from t1) delete from t1 using t1,cte where t1.a=cte.a; + +--echo # +--echo # MDEV-38272 Recursive CTE usage leaks memory when not used in a top level select +--echo # + +--error ER_BAD_FIELD_ERROR +with recursive cte as (select a from t1 union select a from cte) + update t1, cte set t1.o = 1; + +let $restart_file= $MYSQLTEST_VARDIR/tmp/mysqld.1.expect; +--write_line wait $restart_file +--shutdown_server +--source include/wait_until_disconnected.inc +--write_line "restart: " $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +-- enable_reconnect +-- source include/wait_until_connected_again.inc + +--error ER_NON_UPDATABLE_TABLE +with recursive cte as (select a from t1 union select a from cte where a < 2) + delete from cte where a = (select a from cte); + +let $restart_file= $MYSQLTEST_VARDIR/tmp/mysqld.1.expect; +--write_line wait $restart_file +--shutdown_server +--source include/wait_until_disconnected.inc +--write_line "restart: " $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +-- enable_reconnect +-- source include/wait_until_connected_again.inc + +--error ER_NON_UPDATABLE_TABLE +with recursive cte as (select a from t1 union select a from cte where a < 2) + delete from cte where a = 10; + +let $restart_file= $MYSQLTEST_VARDIR/tmp/mysqld.1.expect; +--write_line wait $restart_file +--shutdown_server +--source include/wait_until_disconnected.inc +--write_line "restart: " $MYSQLTEST_VARDIR/tmp/mysqld.1.expect +-- enable_reconnect +-- source include/wait_until_connected_again.inc + +insert into t1 values (1), (5), (10), (15), (20), (25); +with recursive cte as (select a from t1 union select a from cte where a < 2) + delete from v1 using v1, cte where v1.a = cte.a; +select * from t1 order by 1; + +drop table t1; + --echo # End of 12.2 tests diff --git a/sql/sql_base.h b/sql/sql_base.h index 0af7b44001793..32985f319fa07 100644 --- a/sql/sql_base.h +++ b/sql/sql_base.h @@ -456,6 +456,14 @@ class Multiupdate_prelocking_strategy : public DML_prelocking_strategy bool handle_end(THD *thd) override; }; +class Multidelete_prelocking_strategy : public DML_prelocking_strategy +{ + bool done; +public: + void reset(THD *thd) override; + bool handle_end(THD *thd) override; +}; + /** A strategy for prelocking algorithm to be used for LOCK TABLES diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index adcbb27d18fa9..8450900db5e56 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -2208,3 +2208,47 @@ bool Sql_cmd_delete::execute_inner(THD *thd) status_var_add(thd->status_var.rows_sent, thd->get_sent_row_count()); return res; } + + +void Multidelete_prelocking_strategy::reset(THD *thd) +{ + done= false; +} + + +/** + Call setup_tables() to populate lex->first_select_lex()->leaf_tables. + This is needed to properly cleanup the tables used in DELETE's top-level + select. See st_select_lex::cleanup(). +*/ + +bool Multidelete_prelocking_strategy::handle_end(THD *thd) +{ + DBUG_ENTER("Multidelete_prelocking_strategy::handle_end"); + + if (done) + DBUG_RETURN(0); + + LEX *lex= thd->lex; + SELECT_LEX *select_lex= lex->first_select_lex(); + TABLE_LIST *table_list= lex->query_tables; + + done= true; + + /* + This is done to resolve other base tables within derived tables in the + outermost select. + */ + if (mysql_handle_derived(lex, DT_INIT) || + mysql_handle_derived(lex, DT_MERGE_FOR_INSERT) || + mysql_handle_derived(lex, DT_PREPARE)) + DBUG_RETURN(1); + + if (setup_tables(thd, &select_lex->context, &select_lex->top_join_list, + table_list, select_lex->leaf_tables, false, true)) + DBUG_RETURN(1); + + DBUG_RETURN(0); +} + + diff --git a/sql/sql_delete.h b/sql/sql_delete.h index c3afbe8bc6cb3..7b6401a7965f5 100644 --- a/sql/sql_delete.h +++ b/sql/sql_delete.h @@ -56,7 +56,7 @@ class Sql_cmd_delete final : public Sql_cmd_dml DML_prelocking_strategy *get_dml_prelocking_strategy() override { - return &dml_prelocking_strategy; + return &multidelete_prelocking_strategy; } bool processing_as_multitable_delete_prohibited(THD *thd); @@ -106,7 +106,7 @@ class Sql_cmd_delete final : public Sql_cmd_dml bool multitable; /* The prelocking strategy used when opening the used tables */ - DML_prelocking_strategy dml_prelocking_strategy; + Multidelete_prelocking_strategy multidelete_prelocking_strategy; List empty_list; /**< auxiliary empty list used by prepare_inner() */ Protocol *save_protocol; /**< needed for ANALYZE .. DELETE .. RETURNING */ diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index 73d97861325ef..522aa2869ebe3 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -1281,6 +1281,7 @@ void LEX::start(THD *thd_arg) spcont= NULL; proc_list.first= 0; query_tables= 0; + save_list.empty(); reset_query_tables_list(FALSE); clause_that_disallows_subselect= NULL; diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 6a61e9a56f94e..edd818a3a5a93 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -34767,6 +34767,7 @@ bool Sql_cmd_dml::prepare(THD *thd) DBUG_ASSERT(thd->is_error()); DBUG_PRINT("info", ("report_error: %d", thd->is_error())); + free_underlaid_joins(thd, unit->first_select()); (void)unit->cleanup(); return true; @@ -34862,6 +34863,7 @@ bool Sql_cmd_dml::execute(THD *thd) DBUG_ASSERT(thd->is_error() || thd->killed); MYSQL_DML_DONE(thd, 1, 0, 0); THD_STAGE_INFO(thd, stage_end); + free_underlaid_joins(thd, select_lex); // tmp tables allocated in prepare (void)unit->cleanup(); if (is_prepared()) unprepare(thd); diff --git a/sql/sql_union.cc b/sql/sql_union.cc index faffef27a35d2..e485bb9d6e483 100644 --- a/sql/sql_union.cc +++ b/sql/sql_union.cc @@ -2949,7 +2949,12 @@ bool st_select_lex::cleanup() cleanup_window_funcs(window_funcs); - if (join) + /* + In UPDATE/DELETE, it can be that leaf_tables has tables while the JOIN + object is not created yet (and then some error occurs and we get here). + In leaf_tables, recursive CTE references need cleanup. + */ + if (leaf_tables.elements) { List_iterator ti(leaf_tables); TABLE_LIST *tbl; @@ -2966,8 +2971,12 @@ bool st_select_lex::cleanup() error|= (bool) error | (uint) unit->cleanup(); } } + } + + if (join) + { DBUG_ASSERT((st_select_lex*)join->select_lex == this); - error= join->destroy(); + error|= (bool)join->destroy(); delete join; join= 0; } diff --git a/sql/table.cc b/sql/table.cc index 2d5f9f30586a8..83ac675d31139 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -10155,7 +10155,14 @@ bool TABLE_LIST::init_derived(THD *thd, bool init_view) !unit->outer_select()->outer_select(); if (with && updating) + { + /* + We are trying to update or delete from a CTE. This is not allowed. + Don't merge it, then the check for update of non-updatable table + will catch this and report an error. + */ set_materialized_derived(); + } /* In the case where a table merge operation moves a derived table from