/* Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. 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-1301 USA */ #include "sql_truncate.h" #include "sql_priv.h" #include "transaction.h" #include "debug_sync.h" #include "records.h" // READ_RECORD #include "table.h" // TABLE #include "sql_class.h" // THD #include "sql_base.h" // open_and_lock_tables #include "sql_table.h" // write_bin_log #include "sql_handler.h" // mysql_ha_rm_tables #include "datadict.h" // dd_recreate_table() #include "lock.h" // MYSQL_OPEN_TEMPORARY_ONLY /* Delete all rows of a locked table. @param thd Thread context. @param table_list Table list element for the table. @param rows_deleted Whether rows might have been deleted. @retval FALSE Success. @retval TRUE Error. */ static bool delete_all_rows(THD *thd, TABLE *table) { int error; READ_RECORD info; bool is_bulk_delete; bool some_rows_deleted= FALSE; bool save_binlog_row_based= thd->is_current_stmt_binlog_format_row(); DBUG_ENTER("delete_all_rows"); /* Replication of truncate table must be statement based. */ thd->clear_current_stmt_binlog_format_row(); /* Update handler statistics (e.g. table->file->stats.records). Might be used by the storage engine to aggregate information necessary to allow deletion. Currently, this seems to be meaningful only to the archive storage engine, which uses the info method to set the number of records. Although archive does not support deletion, it becomes necessary in order to return a error if the table is not empty. */ error= table->file->info(HA_STATUS_VARIABLE | HA_STATUS_NO_LOCK); if (error && error != HA_ERR_WRONG_COMMAND) { table->file->print_error(error, MYF(0)); goto end; } /* Attempt to delete all rows in the table. If it is unsupported, switch to row by row deletion. */ if (! (error= table->file->ha_delete_all_rows())) goto end; if (error != HA_ERR_WRONG_COMMAND) { /* If a transactional engine fails in the middle of deletion, we expect it to be able to roll it back. Some reasons for the engine to fail would be media failure or corrupted data dictionary (i.e. in case of a partitioned table). We have sufficiently strong metadata locks to rule out any potential deadlocks. If a non-transactional engine fails here (that would not be MyISAM, since MyISAM does TRUNCATE by recreate), and binlog is on, replication breaks, since nothing gets written to the binary log. (XXX: is this a bug?) */ table->file->print_error(error, MYF(0)); goto end; } /* A workaround for Bug#53696 "Performance schema engine violates the PSEA API by calling my_error()". */ if (thd->is_error()) goto end; /* Handler didn't support fast delete. Delete rows one by one. */ init_read_record(&info, thd, table, NULL, TRUE, TRUE, FALSE); /* Start bulk delete. If the engine does not support it, go on, it's not an error. */ is_bulk_delete= ! table->file->start_bulk_delete(); table->mark_columns_needed_for_delete(); while (!(error= info.read_record(&info)) && !thd->killed) { if ((error= table->file->ha_delete_row(table->record[0]))) { table->file->print_error(error, MYF(0)); break; } some_rows_deleted= TRUE; } /* HA_ERR_END_OF_FILE */ if (error == -1) error= 0; /* Close down the bulk delete. */ if (is_bulk_delete) { int bulk_delete_error= table->file->end_bulk_delete(); if (bulk_delete_error && !error) { table->file->print_error(bulk_delete_error, MYF(0)); error= bulk_delete_error; } } end_read_record(&info); /* Regardless of the error status, the query must be written to the binary log if rows of the table is non-transactional. */ if (some_rows_deleted && !table->file->has_transactions()) { thd->transaction.stmt.modified_non_trans_table= TRUE; thd->transaction.all.modified_non_trans_table= TRUE; } if (error || thd->killed) goto end; /* Truncate resets the auto-increment counter. */ error= table->file->ha_reset_auto_increment(0); if (error) { if (error != HA_ERR_WRONG_COMMAND) table->file->print_error(error, MYF(0)); else error= 0; } end: if (save_binlog_row_based) thd->set_current_stmt_binlog_format_row(); DBUG_RETURN(error); } /* Close and recreate a temporary table. In case of success, write truncate statement into the binary log if in statement mode. @param thd Thread context. @param table The temporary table. @retval FALSE Success. @retval TRUE Error. */ static bool recreate_temporary_table(THD *thd, TABLE *table) { bool error= TRUE; TABLE_SHARE *share= table->s; HA_CREATE_INFO create_info; handlerton *table_type= table->s->db_type(); DBUG_ENTER("recreate_temporary_table"); memset(&create_info, 0, sizeof(create_info)); table->file->info(HA_STATUS_AUTO | HA_STATUS_NO_LOCK); /* Don't free share. */ close_temporary_table(thd, table, FALSE, FALSE); /* We must use share->normalized_path.str since for temporary tables it differs from what dd_recreate_table() would generate based on table and schema names. */ ha_create_table(thd, share->normalized_path.str, share->db.str, share->table_name.str, &create_info, 1); if (open_temporary_table(thd, share->path.str, share->db.str, share->table_name.str, 1)) { error= FALSE; thd->thread_specific_used= TRUE; } else rm_temporary_table(table_type, share->path.str); free_table_share(share); my_free(table); DBUG_RETURN(error); } /* Handle opening and locking if 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. @param[out] ticket_downgrade Set if a lock must be downgraded after truncate is done. @retval FALSE Success. @retval TRUE Error. */ static bool open_and_lock_table_for_truncate(THD *thd, TABLE_LIST *table_ref, bool *hton_can_recreate, MDL_ticket **ticket_downgrade) { TABLE *table= NULL; MDL_ticket *mdl_ticket= NULL; DBUG_ENTER("open_and_lock_table_for_truncate"); /* 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->open_tables, table_ref->db, table_ref->table_name, FALSE))) DBUG_RETURN(TRUE); *hton_can_recreate= ha_check_storage_engine_flag(table->s->db_type(), HTON_CAN_RECREATE); } else { /* Even though we could use the previous execution branch here just as well, we must not try to open the table: */ MDL_request mdl_global_request, mdl_request; MDL_request_list mdl_requests; mdl_global_request.init(MDL_key::GLOBAL, "", "", MDL_INTENTION_EXCLUSIVE); mdl_request.init(MDL_key::TABLE, table_ref->db, table_ref->table_name, MDL_SHARED_NO_READ_WRITE); mdl_requests.push_front(&mdl_request); mdl_requests.push_front(&mdl_global_request); if (thd->mdl_context.acquire_locks(&mdl_requests, thd->variables.lock_wait_timeout)) DBUG_RETURN(TRUE); mdl_ticket= mdl_request.ticket; if (dd_check_storage_engine_flag(thd, table_ref->db, table_ref->table_name, HTON_CAN_RECREATE, hton_can_recreate)) DBUG_RETURN(TRUE); } DEBUG_SYNC(thd, "lock_table_for_truncate"); if (*hton_can_recreate) { /* Acquire an exclusive lock. The storage engine can recreate the table only if there are no references to it from anywhere, i.e. no cached TABLE in the table cache. To remove the table from the cache we need an exclusive lock. */ if (thd->locked_tables_mode) { if (wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN)) DBUG_RETURN(TRUE); *ticket_downgrade= table->mdl_ticket; close_all_tables_for_name(thd, table->s, FALSE); } else { ulong timeout= thd->variables.lock_wait_timeout; if (thd->mdl_context.upgrade_shared_lock_to_exclusive(mdl_ticket, timeout)) DBUG_RETURN(TRUE); mysql_mutex_lock(&LOCK_open); tdc_remove_table(thd, TDC_RT_REMOVE_ALL, table_ref->db, table_ref->table_name); mysql_mutex_unlock(&LOCK_open); } } else { /* Can't recreate, we must mechanically delete all rows in the table. 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. Use open_and_lock_tables() to do the necessary job. */ /* Allow to open base tables only. */ table_ref->required_type= FRMTYPE_TABLE; /* We don't need to load triggers. */ DBUG_ASSERT(table_ref->trg_event_map == 0); /* Work around partition parser rules using alter table's. */ if (thd->lex->alter_info.flags & ALTER_ADMIN_PARTITION) { table_ref->lock_type= TL_WRITE; table_ref->mdl_request.set_type(MDL_SHARED_WRITE); } /* Ensure proper lock types (e.g. from the parser). */ DBUG_ASSERT(table_ref->lock_type == TL_WRITE); DBUG_ASSERT(table_ref->mdl_request.type == MDL_SHARED_WRITE); /* Open the table as it will handle some required preparations. 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. */ if (open_and_lock_tables(thd, table_ref, FALSE, MYSQL_OPEN_IGNORE_FLUSH | MYSQL_OPEN_SKIP_TEMPORARY)) DBUG_RETURN(TRUE); } 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 mysql_truncate_table(THD *thd, TABLE_LIST *table_ref) { TABLE *table; bool error= TRUE, binlog_stmt; MDL_ticket *mdl_ticket= NULL; DBUG_ENTER("mysql_truncate_table"); /* Remove tables from the HANDLER's hash. */ mysql_ha_rm_tables(thd, table_ref); /* If it is a temporary table, no need to take locks. */ if ((table= find_temporary_table(thd, table_ref))) { /* In RBR, the statement is not binlogged if the table is temporary. */ binlog_stmt= !thd->is_current_stmt_binlog_format_row(); /* Note that a temporary table cannot be partitioned. */ if (ha_check_storage_engine_flag(table->s->db_type(), HTON_CAN_RECREATE)) { if ((error= recreate_temporary_table(thd, table))) binlog_stmt= FALSE; /* No need to binlog failed truncate-by-recreate. */ DBUG_ASSERT(! thd->transaction.stmt.modified_non_trans_table); } else { /* The engine does not support truncate-by-recreate. Open the table and delete all rows. In such a manner this can in fact open several tables if it's a temporary MyISAMMRG table. */ if (open_and_lock_tables(thd, table_ref, FALSE, MYSQL_OPEN_TEMPORARY_ONLY)) DBUG_RETURN(TRUE); error= delete_all_rows(thd, table_ref->table); } /* 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; if (open_and_lock_table_for_truncate(thd, table_ref, &hton_can_recreate, &mdl_ticket)) 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, table_ref->table_name); if (thd->locked_tables_mode && thd->locked_tables_list.reopen_tables(thd)) thd->locked_tables_list.unlink_all_closed_tables(thd, NULL, 0); /* No need to binlog a failed truncate-by-recreate. */ binlog_stmt= !error; } else { error= delete_all_rows(thd, table_ref->table); /* Regardless of the error status, the query must be written to the binary log if rows of a non-transactional table were deleted. */ binlog_stmt= !error || thd->transaction.stmt.modified_non_trans_table; } 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()); /* All effects of a TRUNCATE TABLE operation are rolled back if a row by row deletion fails. Otherwise, it is automatically committed at the end. */ if (error) { trans_rollback_stmt(thd); trans_rollback(thd); } /* 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 (mdl_ticket) mdl_ticket->downgrade_exclusive_lock(MDL_SHARED_NO_READ_WRITE); DBUG_PRINT("exit", ("error: %d", error)); DBUG_RETURN(test(error)); }