From a175314c3e5827eb193872241446f2f8f5c9d33c Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 May 2024 20:07:14 +0200 Subject: Adding upstream version 1:10.5.12. Signed-off-by: Daniel Baumann --- sql/sql_truncate.cc | 547 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 547 insertions(+) create mode 100644 sql/sql_truncate.cc (limited to 'sql/sql_truncate.cc') diff --git a/sql/sql_truncate.cc b/sql/sql_truncate.cc new file mode 100644 index 00000000..e699869d --- /dev/null +++ b/sql/sql_truncate.cc @@ -0,0 +1,547 @@ +/* Copyright (c) 2010, 2015, Oracle and/or its affiliates. + Copyright (c) 2012, 2018, MariaDB + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#include "mariadb.h" +#include "debug_sync.h" // DEBUG_SYNC +#include "table.h" // TABLE, FOREIGN_KEY_INFO +#include "sql_class.h" // THD +#include "sql_base.h" // open_and_lock_tables +#include "sql_table.h" // write_bin_log +#include "datadict.h" // dd_recreate_table() +#include "lock.h" // MYSQL_OPEN_* flags +#include "sql_acl.h" // DROP_ACL +#include "sql_parse.h" // check_one_table_access() +#include "sql_truncate.h" +#include "wsrep_mysqld.h" +#include "sql_show.h" //append_identifier() +#include "sql_select.h" +#include "sql_delete.h" + +/** + Append a list of field names to a string. + + @param str The string. + @param fields The list of field names. + + @return TRUE on failure, FALSE otherwise. +*/ + +static bool fk_info_append_fields(THD *thd, String *str, + List *fields) +{ + bool res= FALSE; + LEX_CSTRING *field; + List_iterator_fast it(*fields); + + while ((field= it++)) + { + res|= append_identifier(thd, str, field); + res|= str->append(", "); + } + + str->chop(); + str->chop(); + + return res; +} + + +/** + Generate a foreign key description suitable for a error message. + + @param thd Thread context. + @param fk_info The foreign key information. + + @return A human-readable string describing the foreign key. +*/ + +static const char *fk_info_str(THD *thd, FOREIGN_KEY_INFO *fk_info) +{ + bool res= FALSE; + char buffer[STRING_BUFFER_USUAL_SIZE*2]; + String str(buffer, sizeof(buffer), system_charset_info); + + str.length(0); + + /* + `db`.`tbl`, CONSTRAINT `id` FOREIGN KEY (`fk`) REFERENCES `db`.`tbl` (`fk`) + */ + + res|= append_identifier(thd, &str, fk_info->foreign_db); + res|= str.append("."); + res|= append_identifier(thd, &str, fk_info->foreign_table); + res|= str.append(", CONSTRAINT "); + res|= append_identifier(thd, &str, fk_info->foreign_id); + res|= str.append(" FOREIGN KEY ("); + res|= fk_info_append_fields(thd, &str, &fk_info->foreign_fields); + res|= str.append(") REFERENCES "); + res|= append_identifier(thd, &str, fk_info->referenced_db); + res|= str.append("."); + res|= append_identifier(thd, &str, fk_info->referenced_table); + res|= str.append(" ("); + res|= fk_info_append_fields(thd, &str, &fk_info->referenced_fields); + res|= str.append(')'); + + return res ? NULL : thd->strmake(str.ptr(), str.length()); +} + + +/** + Check and emit a fatal error if the table which is going to be + affected by TRUNCATE TABLE is a parent table in some non-self- + referencing foreign key. + + @remark The intention is to allow truncate only for tables that + are not dependent on other tables. + + @param thd Thread context. + @param table Table handle. + + @retval FALSE This table is not parent in a non-self-referencing foreign + key. Statement can proceed. + @retval TRUE This table is parent in a non-self-referencing foreign key, + error was emitted. +*/ + +static bool +fk_truncate_illegal_if_parent(THD *thd, TABLE *table) +{ + FOREIGN_KEY_INFO *fk_info; + List fk_list; + List_iterator_fast it; + + /* + Bail out early if the table is not referenced by a foreign key. + In this case, the table could only be, if at all, a child table. + */ + if (! table->file->referenced_by_foreign_key()) + return FALSE; + + /* + This table _is_ referenced by a foreign key. At this point, only + self-referencing keys are acceptable. For this reason, get the list + of foreign keys referencing this table in order to check the name + of the child (dependent) tables. + */ + table->file->get_parent_foreign_key_list(thd, &fk_list); + + /* Out of memory when building list. */ + if (unlikely(thd->is_error())) + return TRUE; + + it.init(fk_list); + + /* Loop over the set of foreign keys for which this table is a parent. */ + while ((fk_info= it++)) + { + if (lex_string_cmp(system_charset_info, fk_info->referenced_db, + &table->s->db) || + lex_string_cmp(system_charset_info, fk_info->referenced_table, + &table->s->table_name) || + lex_string_cmp(system_charset_info, fk_info->foreign_db, + &table->s->db) || + lex_string_cmp(system_charset_info, fk_info->foreign_table, + &table->s->table_name)) + break; + } + + /* Table is parent in a non-self-referencing foreign key. */ + if (fk_info) + { + my_error(ER_TRUNCATE_ILLEGAL_FK, MYF(0), fk_info_str(thd, fk_info)); + return TRUE; + } + + return FALSE; +} + + +/* + Open and truncate a locked table. + + @param thd Thread context. + @param table_ref Table list element for the table to be truncated. + @param is_tmp_table True if element refers to a temp table. + + @retval TRUNCATE_OK Truncate was successful and statement can be safely + binlogged. + @retval TRUNCATE_FAILED_BUT_BINLOG Truncate failed but still go ahead with + binlogging as in case of non transactional tables + partial truncation is possible. + + @retval TRUNCATE_FAILED_SKIP_BINLOG Truncate was not successful hence donot + binlong the statement. +*/ + +enum Sql_cmd_truncate_table::truncate_result +Sql_cmd_truncate_table::handler_truncate(THD *thd, TABLE_LIST *table_ref, + bool is_tmp_table) +{ + int error= 0; + uint flags= 0; + DBUG_ENTER("Sql_cmd_truncate_table::handler_truncate"); + + /* + Can't recreate, the engine must mechanically delete all rows + in the table. Use open_and_lock_tables() to open a write cursor. + */ + + /* If it is a temporary table, no need to take locks. */ + if (!is_tmp_table) + { + /* We don't need to load triggers. */ + DBUG_ASSERT(table_ref->trg_event_map == 0); + /* + Our metadata lock guarantees that no transaction is reading + or writing into the table. Yet, to open a write cursor we need + a thr_lock lock. Allow to open base tables only. + */ + table_ref->required_type= TABLE_TYPE_NORMAL; + /* + Ignore pending FLUSH TABLES since we don't want to release + the MDL lock taken above and otherwise there is no way to + wait for FLUSH TABLES in deadlock-free fashion. + */ + flags= MYSQL_OPEN_IGNORE_FLUSH; + /* + Even though we have an MDL lock on the table here, we don't + pass MYSQL_OPEN_HAS_MDL_LOCK to open_and_lock_tables + since to truncate a MERGE table, we must open and lock + merge children, and on those we don't have an MDL lock. + Thus clear the ticket to satisfy MDL asserts. + */ + table_ref->mdl_request.ticket= NULL; + } + + /* Open the table as it will handle some required preparations. */ + if (open_and_lock_tables(thd, table_ref, FALSE, flags)) + DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG); + + /* Whether to truncate regardless of foreign keys. */ + if (! (thd->variables.option_bits & OPTION_NO_FOREIGN_KEY_CHECKS)) + if (fk_truncate_illegal_if_parent(thd, table_ref->table)) + DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG); + + error= table_ref->table->file->ha_truncate(); + if (unlikely(error)) + { + table_ref->table->file->print_error(error, MYF(0)); + /* + If truncate method is not implemented then we don't binlog the + statement. If truncation has failed in a transactional engine then also + we don't binlog the statment. Only in non transactional engine we binlog + inspite of errors. + */ + if (error == HA_ERR_WRONG_COMMAND || + table_ref->table->file->has_transactions_and_rollback()) + DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG); + else + DBUG_RETURN(TRUNCATE_FAILED_BUT_BINLOG); + } + DBUG_RETURN(TRUNCATE_OK); +} + + +/* + Handle locking a base table for truncate. + + @param[in] thd Thread context. + @param[in] table_ref Table list element for the table to + be truncated. + @param[out] hton_can_recreate Set to TRUE if table can be dropped + and recreated. + + @retval FALSE Success. + @retval TRUE Error. +*/ + +bool Sql_cmd_truncate_table::lock_table(THD *thd, TABLE_LIST *table_ref, + bool *hton_can_recreate) +{ + handlerton *hton; + bool versioned; + bool sequence= false; + TABLE *table= NULL; + DBUG_ENTER("Sql_cmd_truncate_table::lock_table"); + + /* Lock types are set in the parser. */ + DBUG_ASSERT(table_ref->lock_type == TL_WRITE); + /* The handler truncate protocol dictates a exclusive lock. */ + DBUG_ASSERT(table_ref->mdl_request.type == MDL_EXCLUSIVE); + + /* + Before doing anything else, acquire a metadata lock on the table, + or ensure we have one. We don't use open_and_lock_tables() + right away because we want to be able to truncate (and recreate) + corrupted tables, those that we can't fully open. + + MySQL manual documents that TRUNCATE can be used to repair a + damaged table, i.e. a table that can not be fully "opened". + In particular MySQL manual says: As long as the table format + file tbl_name.frm is valid, the table can be re-created as + an empty table with TRUNCATE TABLE, even if the data or index + files have become corrupted. + */ + if (thd->locked_tables_mode) + { + if (!(table= find_table_for_mdl_upgrade(thd, table_ref->db.str, + table_ref->table_name.str, NULL))) + DBUG_RETURN(TRUE); + + versioned= table->versioned(); + hton= table->file->ht; +#ifdef WITH_WSREP + if (WSREP(thd) && + !wsrep_should_replicate_ddl(thd, hton->db_type)) + DBUG_RETURN(TRUE); +#endif + + table_ref->mdl_request.ticket= table->mdl_ticket; + } + else + { + DBUG_ASSERT(table_ref->next_global == NULL); + if (lock_table_names(thd, table_ref, NULL, + thd->variables.lock_wait_timeout, 0)) + DBUG_RETURN(TRUE); + + TABLE_SHARE *share= tdc_acquire_share(thd, table_ref, GTS_TABLE | GTS_VIEW); + if (share == NULL) + DBUG_RETURN(TRUE); + DBUG_ASSERT(share != UNUSABLE_TABLE_SHARE); + + versioned= share->versioned; + sequence= share->table_type == TABLE_TYPE_SEQUENCE; + hton= share->db_type(); +#ifdef WITH_WSREP + if (WSREP(thd) && + hton != view_pseudo_hton && + !wsrep_should_replicate_ddl(thd, hton->db_type)) + { + tdc_release_share(share); + DBUG_RETURN(TRUE); + } +#endif + + if (!versioned) + tdc_remove_referenced_share(thd, share); + else + tdc_release_share(share); + + if (hton == view_pseudo_hton) + { + my_error(ER_NO_SUCH_TABLE, MYF(0), table_ref->db.str, + table_ref->table_name.str); + DBUG_RETURN(TRUE); + } + } + + *hton_can_recreate= !sequence + && ha_check_storage_engine_flag(hton, HTON_CAN_RECREATE); + + if (versioned) + { + my_error(ER_VERS_NOT_SUPPORTED, MYF(0), "TRUNCATE TABLE"); + DBUG_RETURN(TRUE); + } + + /* + A storage engine can recreate or truncate the table only if there + are no references to it from anywhere, i.e. no cached TABLE in the + table cache. + */ + if (thd->locked_tables_mode) + { + DEBUG_SYNC(thd, "upgrade_lock_for_truncate"); + /* To remove the table from the cache we need an exclusive lock. */ + if (wait_while_table_is_used(thd, table, + *hton_can_recreate ? HA_EXTRA_PREPARE_FOR_DROP : HA_EXTRA_NOT_USED)) + DBUG_RETURN(TRUE); + m_ticket_downgrade= table->mdl_ticket; + /* Close if table is going to be recreated. */ + if (*hton_can_recreate) + close_all_tables_for_name(thd, table->s, HA_EXTRA_NOT_USED, NULL); + } + DBUG_RETURN(FALSE); +} + + +/* + Optimized delete of all rows by doing a full generate of the table. + + @remark Will work even if the .MYI and .MYD files are destroyed. + In other words, it works as long as the .FRM is intact and + the engine supports re-create. + + @param thd Thread context. + @param table_ref Table list element for the table to be truncated. + + @retval FALSE Success. + @retval TRUE Error. +*/ + +bool Sql_cmd_truncate_table::truncate_table(THD *thd, TABLE_LIST *table_ref) +{ + int error; + bool binlog_stmt; + DBUG_ENTER("Sql_cmd_truncate_table::truncate_table"); + + DBUG_ASSERT((!table_ref->table) || + (table_ref->table && table_ref->table->s)); + + /* Initialize, or reinitialize in case of reexecution (SP). */ + m_ticket_downgrade= NULL; + + /* If it is a temporary table, no need to take locks. */ + if (is_temporary_table(table_ref)) + { + /* In RBR, the statement is not binlogged if the table is temporary. */ + binlog_stmt= !thd->is_current_stmt_binlog_format_row(); + + thd->close_unused_temporary_table_instances(table_ref); + + error= handler_truncate(thd, table_ref, TRUE); + + /* + No need to invalidate the query cache, queries with temporary + tables are not in the cache. No need to write to the binary + log a failed row-by-row delete even if under RBR as the table + might not exist on the slave. + */ + } + else /* It's not a temporary table. */ + { + bool hton_can_recreate; + +#ifdef WITH_WSREP + if (WSREP(thd) && wsrep_thd_is_local(thd)) + { + wsrep::key_array keys; + /* Do not start TOI if table is not found */ + if (!wsrep_append_fk_parent_table(thd, table_ref, &keys)) + { + if (keys.empty()) + { + WSREP_TO_ISOLATION_BEGIN_IF(table_ref->db.str, table_ref->table_name.str, NULL) + { + DBUG_RETURN(TRUE); + } + } else { + WSREP_TO_ISOLATION_BEGIN_FK_TABLES(NULL, NULL, table_ref, &keys) + { + DBUG_RETURN(TRUE); + } + } + } + } +#endif /* WITH_WSREP */ + + if (lock_table(thd, table_ref, &hton_can_recreate)) + DBUG_RETURN(TRUE); + + if (hton_can_recreate) + { + /* + The storage engine can truncate the table by creating an + empty table with the same structure. + */ + error= dd_recreate_table(thd, table_ref->db.str, table_ref->table_name.str); + + if (thd->locked_tables_mode && thd->locked_tables_list.reopen_tables(thd, false)) + { + thd->locked_tables_list.unlink_all_closed_tables(thd, NULL, 0); + error=1; + } + + /* No need to binlog a failed truncate-by-recreate. */ + binlog_stmt= !error; + } + else + { + /* + The engine does not support truncate-by-recreate. + Attempt to use the handler truncate method. + */ + error= handler_truncate(thd, table_ref, FALSE); + + if (error == TRUNCATE_OK && thd->locked_tables_mode && + (table_ref->table->file->ht->flags & + HTON_REQUIRES_CLOSE_AFTER_TRUNCATE)) + { + thd->locked_tables_list.mark_table_for_reopen(thd, table_ref->table); + if (unlikely(thd->locked_tables_list.reopen_tables(thd, true))) + thd->locked_tables_list.unlink_all_closed_tables(thd, NULL, 0); + } + + /* + All effects of a TRUNCATE TABLE operation are committed even if + truncation fails in the case of non transactional tables. Thus, the + query must be written to the binary log. The only exception is a + unimplemented truncate method. + */ + if (unlikely(error == TRUNCATE_OK || error == TRUNCATE_FAILED_BUT_BINLOG)) + binlog_stmt= true; + else + binlog_stmt= false; + } + + /* + If we tried to open a MERGE table and failed due to problems with the + children tables, the table will have been closed and table_ref->table + will be invalid. Reset the pointer here in any case as + query_cache_invalidate does not need a valid TABLE object. + */ + table_ref->table= NULL; + query_cache_invalidate3(thd, table_ref, FALSE); + } + + /* DDL is logged in statement format, regardless of binlog format. */ + if (binlog_stmt) + error|= write_bin_log(thd, !error, thd->query(), thd->query_length()); + + /* + A locked table ticket was upgraded to a exclusive lock. After the + the query has been written to the binary log, downgrade the lock + to a shared one. + */ + if (m_ticket_downgrade) + m_ticket_downgrade->downgrade_lock(MDL_SHARED_NO_READ_WRITE); + + DBUG_RETURN(error); +} + +/** + Execute a TRUNCATE statement at runtime. + + @param thd The current thread. + + @return FALSE on success. +*/ + +bool Sql_cmd_truncate_table::execute(THD *thd) +{ + bool res= TRUE; + TABLE_LIST *table= thd->lex->first_select_lex()->table_list.first; + DBUG_ENTER("Sql_cmd_truncate_table::execute"); + + if (check_one_table_access(thd, DROP_ACL, table)) + DBUG_RETURN(res); + + if (! (res= truncate_table(thd, table))) + my_ok(thd); + + DBUG_RETURN(res); +} -- cgit v1.2.3