| 1 | /* Copyright (c) 2010, 2015, Oracle and/or its affiliates. |
| 2 | Copyright (c) 2012, 2018, MariaDB |
| 3 | |
| 4 | This program is free software; you can redistribute it and/or modify |
| 5 | it under the terms of the GNU General Public License as published by |
| 6 | the Free Software Foundation; version 2 of the License. |
| 7 | |
| 8 | This program is distributed in the hope that it will be useful, |
| 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 | GNU General Public License for more details. |
| 12 | |
| 13 | You should have received a copy of the GNU General Public License |
| 14 | along with this program; if not, write to the Free Software |
| 15 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ |
| 16 | |
| 17 | #include "mariadb.h" |
| 18 | #include "debug_sync.h" // DEBUG_SYNC |
| 19 | #include "table.h" // TABLE, FOREIGN_KEY_INFO |
| 20 | #include "sql_class.h" // THD |
| 21 | #include "sql_base.h" // open_and_lock_tables |
| 22 | #include "sql_table.h" // write_bin_log |
| 23 | #include "datadict.h" // dd_recreate_table() |
| 24 | #include "lock.h" // MYSQL_OPEN_* flags |
| 25 | #include "sql_acl.h" // DROP_ACL |
| 26 | #include "sql_parse.h" // check_one_table_access() |
| 27 | #include "sql_truncate.h" |
| 28 | #include "wsrep_mysqld.h" |
| 29 | #include "sql_show.h" //append_identifier() |
| 30 | #include "sql_select.h" |
| 31 | #include "sql_delete.h" |
| 32 | |
| 33 | /** |
| 34 | Append a list of field names to a string. |
| 35 | |
| 36 | @param str The string. |
| 37 | @param fields The list of field names. |
| 38 | |
| 39 | @return TRUE on failure, FALSE otherwise. |
| 40 | */ |
| 41 | |
| 42 | static bool fk_info_append_fields(THD *thd, String *str, |
| 43 | List<LEX_CSTRING> *fields) |
| 44 | { |
| 45 | bool res= FALSE; |
| 46 | LEX_CSTRING *field; |
| 47 | List_iterator_fast<LEX_CSTRING> it(*fields); |
| 48 | |
| 49 | while ((field= it++)) |
| 50 | { |
| 51 | res|= append_identifier(thd, str, field); |
| 52 | res|= str->append(", " ); |
| 53 | } |
| 54 | |
| 55 | str->chop(); |
| 56 | str->chop(); |
| 57 | |
| 58 | return res; |
| 59 | } |
| 60 | |
| 61 | |
| 62 | /** |
| 63 | Generate a foreign key description suitable for a error message. |
| 64 | |
| 65 | @param thd Thread context. |
| 66 | @param fk_info The foreign key information. |
| 67 | |
| 68 | @return A human-readable string describing the foreign key. |
| 69 | */ |
| 70 | |
| 71 | static const char *fk_info_str(THD *thd, FOREIGN_KEY_INFO *fk_info) |
| 72 | { |
| 73 | bool res= FALSE; |
| 74 | char buffer[STRING_BUFFER_USUAL_SIZE*2]; |
| 75 | String str(buffer, sizeof(buffer), system_charset_info); |
| 76 | |
| 77 | str.length(0); |
| 78 | |
| 79 | /* |
| 80 | `db`.`tbl`, CONSTRAINT `id` FOREIGN KEY (`fk`) REFERENCES `db`.`tbl` (`fk`) |
| 81 | */ |
| 82 | |
| 83 | res|= append_identifier(thd, &str, fk_info->foreign_db); |
| 84 | res|= str.append("." ); |
| 85 | res|= append_identifier(thd, &str, fk_info->foreign_table); |
| 86 | res|= str.append(", CONSTRAINT " ); |
| 87 | res|= append_identifier(thd, &str, fk_info->foreign_id); |
| 88 | res|= str.append(" FOREIGN KEY (" ); |
| 89 | res|= fk_info_append_fields(thd, &str, &fk_info->foreign_fields); |
| 90 | res|= str.append(") REFERENCES " ); |
| 91 | res|= append_identifier(thd, &str, fk_info->referenced_db); |
| 92 | res|= str.append("." ); |
| 93 | res|= append_identifier(thd, &str, fk_info->referenced_table); |
| 94 | res|= str.append(" (" ); |
| 95 | res|= fk_info_append_fields(thd, &str, &fk_info->referenced_fields); |
| 96 | res|= str.append(')'); |
| 97 | |
| 98 | return res ? NULL : thd->strmake(str.ptr(), str.length()); |
| 99 | } |
| 100 | |
| 101 | |
| 102 | /** |
| 103 | Check and emit a fatal error if the table which is going to be |
| 104 | affected by TRUNCATE TABLE is a parent table in some non-self- |
| 105 | referencing foreign key. |
| 106 | |
| 107 | @remark The intention is to allow truncate only for tables that |
| 108 | are not dependent on other tables. |
| 109 | |
| 110 | @param thd Thread context. |
| 111 | @param table Table handle. |
| 112 | |
| 113 | @retval FALSE This table is not parent in a non-self-referencing foreign |
| 114 | key. Statement can proceed. |
| 115 | @retval TRUE This table is parent in a non-self-referencing foreign key, |
| 116 | error was emitted. |
| 117 | */ |
| 118 | |
| 119 | static bool |
| 120 | fk_truncate_illegal_if_parent(THD *thd, TABLE *table) |
| 121 | { |
| 122 | FOREIGN_KEY_INFO *fk_info; |
| 123 | List<FOREIGN_KEY_INFO> fk_list; |
| 124 | List_iterator_fast<FOREIGN_KEY_INFO> it; |
| 125 | |
| 126 | /* |
| 127 | Bail out early if the table is not referenced by a foreign key. |
| 128 | In this case, the table could only be, if at all, a child table. |
| 129 | */ |
| 130 | if (! table->file->referenced_by_foreign_key()) |
| 131 | return FALSE; |
| 132 | |
| 133 | /* |
| 134 | This table _is_ referenced by a foreign key. At this point, only |
| 135 | self-referencing keys are acceptable. For this reason, get the list |
| 136 | of foreign keys referencing this table in order to check the name |
| 137 | of the child (dependent) tables. |
| 138 | */ |
| 139 | table->file->get_parent_foreign_key_list(thd, &fk_list); |
| 140 | |
| 141 | /* Out of memory when building list. */ |
| 142 | if (unlikely(thd->is_error())) |
| 143 | return TRUE; |
| 144 | |
| 145 | it.init(fk_list); |
| 146 | |
| 147 | /* Loop over the set of foreign keys for which this table is a parent. */ |
| 148 | while ((fk_info= it++)) |
| 149 | { |
| 150 | DBUG_ASSERT(!lex_string_cmp(system_charset_info, |
| 151 | fk_info->referenced_db, |
| 152 | &table->s->db)); |
| 153 | |
| 154 | DBUG_ASSERT(!lex_string_cmp(system_charset_info, |
| 155 | fk_info->referenced_table, |
| 156 | &table->s->table_name)); |
| 157 | |
| 158 | if (lex_string_cmp(system_charset_info, fk_info->foreign_db, |
| 159 | &table->s->db) || |
| 160 | lex_string_cmp(system_charset_info, fk_info->foreign_table, |
| 161 | &table->s->table_name)) |
| 162 | break; |
| 163 | } |
| 164 | |
| 165 | /* Table is parent in a non-self-referencing foreign key. */ |
| 166 | if (fk_info) |
| 167 | { |
| 168 | my_error(ER_TRUNCATE_ILLEGAL_FK, MYF(0), fk_info_str(thd, fk_info)); |
| 169 | return TRUE; |
| 170 | } |
| 171 | |
| 172 | return FALSE; |
| 173 | } |
| 174 | |
| 175 | |
| 176 | /* |
| 177 | Open and truncate a locked table. |
| 178 | |
| 179 | @param thd Thread context. |
| 180 | @param table_ref Table list element for the table to be truncated. |
| 181 | @param is_tmp_table True if element refers to a temp table. |
| 182 | |
| 183 | @retval TRUNCATE_OK Truncate was successful and statement can be safely |
| 184 | binlogged. |
| 185 | @retval TRUNCATE_FAILED_BUT_BINLOG Truncate failed but still go ahead with |
| 186 | binlogging as in case of non transactional tables |
| 187 | partial truncation is possible. |
| 188 | |
| 189 | @retval TRUNCATE_FAILED_SKIP_BINLOG Truncate was not successful hence donot |
| 190 | binlong the statement. |
| 191 | */ |
| 192 | |
| 193 | enum Sql_cmd_truncate_table::truncate_result |
| 194 | Sql_cmd_truncate_table::handler_truncate(THD *thd, TABLE_LIST *table_ref, |
| 195 | bool is_tmp_table) |
| 196 | { |
| 197 | int error= 0; |
| 198 | uint flags= 0; |
| 199 | DBUG_ENTER("Sql_cmd_truncate_table::handler_truncate" ); |
| 200 | |
| 201 | /* |
| 202 | Can't recreate, the engine must mechanically delete all rows |
| 203 | in the table. Use open_and_lock_tables() to open a write cursor. |
| 204 | */ |
| 205 | |
| 206 | /* If it is a temporary table, no need to take locks. */ |
| 207 | if (!is_tmp_table) |
| 208 | { |
| 209 | /* We don't need to load triggers. */ |
| 210 | DBUG_ASSERT(table_ref->trg_event_map == 0); |
| 211 | /* |
| 212 | Our metadata lock guarantees that no transaction is reading |
| 213 | or writing into the table. Yet, to open a write cursor we need |
| 214 | a thr_lock lock. Allow to open base tables only. |
| 215 | */ |
| 216 | table_ref->required_type= TABLE_TYPE_NORMAL; |
| 217 | /* |
| 218 | Ignore pending FLUSH TABLES since we don't want to release |
| 219 | the MDL lock taken above and otherwise there is no way to |
| 220 | wait for FLUSH TABLES in deadlock-free fashion. |
| 221 | */ |
| 222 | flags= MYSQL_OPEN_IGNORE_FLUSH; |
| 223 | /* |
| 224 | Even though we have an MDL lock on the table here, we don't |
| 225 | pass MYSQL_OPEN_HAS_MDL_LOCK to open_and_lock_tables |
| 226 | since to truncate a MERGE table, we must open and lock |
| 227 | merge children, and on those we don't have an MDL lock. |
| 228 | Thus clear the ticket to satisfy MDL asserts. |
| 229 | */ |
| 230 | table_ref->mdl_request.ticket= NULL; |
| 231 | } |
| 232 | |
| 233 | /* Open the table as it will handle some required preparations. */ |
| 234 | if (open_and_lock_tables(thd, table_ref, FALSE, flags)) |
| 235 | DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG); |
| 236 | |
| 237 | /* Whether to truncate regardless of foreign keys. */ |
| 238 | if (! (thd->variables.option_bits & OPTION_NO_FOREIGN_KEY_CHECKS)) |
| 239 | if (fk_truncate_illegal_if_parent(thd, table_ref->table)) |
| 240 | DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG); |
| 241 | |
| 242 | error= table_ref->table->file->ha_truncate(); |
| 243 | if (unlikely(error)) |
| 244 | { |
| 245 | table_ref->table->file->print_error(error, MYF(0)); |
| 246 | /* |
| 247 | If truncate method is not implemented then we don't binlog the |
| 248 | statement. If truncation has failed in a transactional engine then also |
| 249 | we don't binlog the statment. Only in non transactional engine we binlog |
| 250 | inspite of errors. |
| 251 | */ |
| 252 | if (error == HA_ERR_WRONG_COMMAND || |
| 253 | table_ref->table->file->has_transactions()) |
| 254 | DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG); |
| 255 | else |
| 256 | DBUG_RETURN(TRUNCATE_FAILED_BUT_BINLOG); |
| 257 | } |
| 258 | DBUG_RETURN(TRUNCATE_OK); |
| 259 | } |
| 260 | |
| 261 | |
| 262 | /* |
| 263 | Handle locking a base table for truncate. |
| 264 | |
| 265 | @param[in] thd Thread context. |
| 266 | @param[in] table_ref Table list element for the table to |
| 267 | be truncated. |
| 268 | @param[out] hton_can_recreate Set to TRUE if table can be dropped |
| 269 | and recreated. |
| 270 | |
| 271 | @retval FALSE Success. |
| 272 | @retval TRUE Error. |
| 273 | */ |
| 274 | |
| 275 | bool Sql_cmd_truncate_table::lock_table(THD *thd, TABLE_LIST *table_ref, |
| 276 | bool *hton_can_recreate) |
| 277 | { |
| 278 | TABLE *table= NULL; |
| 279 | DBUG_ENTER("Sql_cmd_truncate_table::lock_table" ); |
| 280 | |
| 281 | /* Lock types are set in the parser. */ |
| 282 | DBUG_ASSERT(table_ref->lock_type == TL_WRITE); |
| 283 | /* The handler truncate protocol dictates a exclusive lock. */ |
| 284 | DBUG_ASSERT(table_ref->mdl_request.type == MDL_EXCLUSIVE); |
| 285 | |
| 286 | /* |
| 287 | Before doing anything else, acquire a metadata lock on the table, |
| 288 | or ensure we have one. We don't use open_and_lock_tables() |
| 289 | right away because we want to be able to truncate (and recreate) |
| 290 | corrupted tables, those that we can't fully open. |
| 291 | |
| 292 | MySQL manual documents that TRUNCATE can be used to repair a |
| 293 | damaged table, i.e. a table that can not be fully "opened". |
| 294 | In particular MySQL manual says: As long as the table format |
| 295 | file tbl_name.frm is valid, the table can be re-created as |
| 296 | an empty table with TRUNCATE TABLE, even if the data or index |
| 297 | files have become corrupted. |
| 298 | */ |
| 299 | if (thd->locked_tables_mode) |
| 300 | { |
| 301 | if (!(table= find_table_for_mdl_upgrade(thd, table_ref->db.str, |
| 302 | table_ref->table_name.str, FALSE))) |
| 303 | DBUG_RETURN(TRUE); |
| 304 | |
| 305 | *hton_can_recreate= ha_check_storage_engine_flag(table->file->ht, |
| 306 | HTON_CAN_RECREATE); |
| 307 | table_ref->mdl_request.ticket= table->mdl_ticket; |
| 308 | } |
| 309 | else |
| 310 | { |
| 311 | handlerton *hton; |
| 312 | bool is_sequence; |
| 313 | |
| 314 | /* Acquire an exclusive lock. */ |
| 315 | DBUG_ASSERT(table_ref->next_global == NULL); |
| 316 | if (lock_table_names(thd, table_ref, NULL, |
| 317 | thd->variables.lock_wait_timeout, 0)) |
| 318 | DBUG_RETURN(TRUE); |
| 319 | |
| 320 | if (!ha_table_exists(thd, &table_ref->db, &table_ref->table_name, |
| 321 | &hton, &is_sequence) || |
| 322 | hton == view_pseudo_hton) |
| 323 | { |
| 324 | my_error(ER_NO_SUCH_TABLE, MYF(0), table_ref->db.str, |
| 325 | table_ref->table_name.str); |
| 326 | DBUG_RETURN(TRUE); |
| 327 | } |
| 328 | |
| 329 | if (!hton) |
| 330 | { |
| 331 | /* |
| 332 | The table exists, but its storage engine is unknown, perhaps not |
| 333 | loaded at the moment. We need to open and parse the frm to know the |
| 334 | storage engine in question, so let's proceed with the truncation and |
| 335 | try to open the table. This will produce the correct error message |
| 336 | about unknown engine. |
| 337 | */ |
| 338 | *hton_can_recreate= false; |
| 339 | } |
| 340 | else |
| 341 | *hton_can_recreate= !is_sequence && hton->flags & HTON_CAN_RECREATE; |
| 342 | } |
| 343 | |
| 344 | /* |
| 345 | A storage engine can recreate or truncate the table only if there |
| 346 | are no references to it from anywhere, i.e. no cached TABLE in the |
| 347 | table cache. |
| 348 | */ |
| 349 | if (thd->locked_tables_mode) |
| 350 | { |
| 351 | DEBUG_SYNC(thd, "upgrade_lock_for_truncate" ); |
| 352 | /* To remove the table from the cache we need an exclusive lock. */ |
| 353 | if (wait_while_table_is_used(thd, table, HA_EXTRA_PREPARE_FOR_DROP)) |
| 354 | DBUG_RETURN(TRUE); |
| 355 | m_ticket_downgrade= table->mdl_ticket; |
| 356 | /* Close if table is going to be recreated. */ |
| 357 | if (*hton_can_recreate) |
| 358 | close_all_tables_for_name(thd, table->s, HA_EXTRA_NOT_USED, NULL); |
| 359 | } |
| 360 | else |
| 361 | { |
| 362 | /* Table is already locked exclusively. Remove cached instances. */ |
| 363 | tdc_remove_table(thd, TDC_RT_REMOVE_ALL, table_ref->db.str, |
| 364 | table_ref->table_name.str, FALSE); |
| 365 | } |
| 366 | |
| 367 | DBUG_RETURN(FALSE); |
| 368 | } |
| 369 | |
| 370 | |
| 371 | /* |
| 372 | Optimized delete of all rows by doing a full generate of the table. |
| 373 | |
| 374 | @remark Will work even if the .MYI and .MYD files are destroyed. |
| 375 | In other words, it works as long as the .FRM is intact and |
| 376 | the engine supports re-create. |
| 377 | |
| 378 | @param thd Thread context. |
| 379 | @param table_ref Table list element for the table to be truncated. |
| 380 | |
| 381 | @retval FALSE Success. |
| 382 | @retval TRUE Error. |
| 383 | */ |
| 384 | |
| 385 | bool Sql_cmd_truncate_table::truncate_table(THD *thd, TABLE_LIST *table_ref) |
| 386 | { |
| 387 | int error; |
| 388 | bool binlog_stmt; |
| 389 | DBUG_ENTER("Sql_cmd_truncate_table::truncate_table" ); |
| 390 | |
| 391 | DBUG_ASSERT((!table_ref->table) || |
| 392 | (table_ref->table && table_ref->table->s)); |
| 393 | |
| 394 | /* Initialize, or reinitialize in case of reexecution (SP). */ |
| 395 | m_ticket_downgrade= NULL; |
| 396 | |
| 397 | /* If it is a temporary table, no need to take locks. */ |
| 398 | if (is_temporary_table(table_ref)) |
| 399 | { |
| 400 | /* In RBR, the statement is not binlogged if the table is temporary. */ |
| 401 | binlog_stmt= !thd->is_current_stmt_binlog_format_row(); |
| 402 | |
| 403 | error= handler_truncate(thd, table_ref, TRUE); |
| 404 | |
| 405 | /* |
| 406 | No need to invalidate the query cache, queries with temporary |
| 407 | tables are not in the cache. No need to write to the binary |
| 408 | log a failed row-by-row delete even if under RBR as the table |
| 409 | might not exist on the slave. |
| 410 | */ |
| 411 | } |
| 412 | else /* It's not a temporary table. */ |
| 413 | { |
| 414 | bool hton_can_recreate; |
| 415 | |
| 416 | if (WSREP(thd) && |
| 417 | wsrep_to_isolation_begin(thd, table_ref->db.str, table_ref->table_name.str, 0)) |
| 418 | DBUG_RETURN(TRUE); |
| 419 | if (lock_table(thd, table_ref, &hton_can_recreate)) |
| 420 | DBUG_RETURN(TRUE); |
| 421 | |
| 422 | if (hton_can_recreate) |
| 423 | { |
| 424 | /* |
| 425 | The storage engine can truncate the table by creating an |
| 426 | empty table with the same structure. |
| 427 | */ |
| 428 | error= dd_recreate_table(thd, table_ref->db.str, table_ref->table_name.str); |
| 429 | |
| 430 | if (thd->locked_tables_mode && thd->locked_tables_list.reopen_tables(thd, false)) |
| 431 | { |
| 432 | thd->locked_tables_list.unlink_all_closed_tables(thd, NULL, 0); |
| 433 | error=1; |
| 434 | } |
| 435 | |
| 436 | /* No need to binlog a failed truncate-by-recreate. */ |
| 437 | binlog_stmt= !error; |
| 438 | } |
| 439 | else |
| 440 | { |
| 441 | /* |
| 442 | The engine does not support truncate-by-recreate. |
| 443 | Attempt to use the handler truncate method. |
| 444 | */ |
| 445 | error= handler_truncate(thd, table_ref, FALSE); |
| 446 | |
| 447 | /* |
| 448 | All effects of a TRUNCATE TABLE operation are committed even if |
| 449 | truncation fails in the case of non transactional tables. Thus, the |
| 450 | query must be written to the binary log. The only exception is a |
| 451 | unimplemented truncate method. |
| 452 | */ |
| 453 | if (unlikely(error == TRUNCATE_OK || error == TRUNCATE_FAILED_BUT_BINLOG)) |
| 454 | binlog_stmt= true; |
| 455 | else |
| 456 | binlog_stmt= false; |
| 457 | } |
| 458 | |
| 459 | /* |
| 460 | If we tried to open a MERGE table and failed due to problems with the |
| 461 | children tables, the table will have been closed and table_ref->table |
| 462 | will be invalid. Reset the pointer here in any case as |
| 463 | query_cache_invalidate does not need a valid TABLE object. |
| 464 | */ |
| 465 | table_ref->table= NULL; |
| 466 | query_cache_invalidate3(thd, table_ref, FALSE); |
| 467 | } |
| 468 | |
| 469 | /* DDL is logged in statement format, regardless of binlog format. */ |
| 470 | if (binlog_stmt) |
| 471 | error|= write_bin_log(thd, !error, thd->query(), thd->query_length()); |
| 472 | |
| 473 | /* |
| 474 | A locked table ticket was upgraded to a exclusive lock. After the |
| 475 | the query has been written to the binary log, downgrade the lock |
| 476 | to a shared one. |
| 477 | */ |
| 478 | if (m_ticket_downgrade) |
| 479 | m_ticket_downgrade->downgrade_lock(MDL_SHARED_NO_READ_WRITE); |
| 480 | |
| 481 | DBUG_RETURN(error); |
| 482 | } |
| 483 | |
| 484 | /** |
| 485 | Execute a TRUNCATE statement at runtime. |
| 486 | |
| 487 | @param thd The current thread. |
| 488 | |
| 489 | @return FALSE on success. |
| 490 | */ |
| 491 | |
| 492 | bool Sql_cmd_truncate_table::execute(THD *thd) |
| 493 | { |
| 494 | bool res= TRUE; |
| 495 | TABLE_LIST *table= thd->lex->select_lex.table_list.first; |
| 496 | DBUG_ENTER("Sql_cmd_truncate_table::execute" ); |
| 497 | |
| 498 | if (check_one_table_access(thd, DROP_ACL, table)) |
| 499 | DBUG_RETURN(res); |
| 500 | |
| 501 | if (! (res= truncate_table(thd, table))) |
| 502 | my_ok(thd); |
| 503 | |
| 504 | DBUG_RETURN(res); |
| 505 | } |
| 506 | |